¿Crear automáticamente una enumeración basada en valores en una tabla de búsqueda de base de datos?

116

¿Cómo creo automáticamente una enumeración y luego uso sus valores en C # según los valores en una tabla de búsqueda de base de datos (usando la capa de datos de la biblioteca empresarial)?

Por ejemplo, si agrego un nuevo valor de búsqueda en la base de datos, no quiero tener que agregar manualmente la declaración de valor de enumeración estática adicional en el código; me gustaría mantener la enumeración sincronizada con la base de datos.

¿Existe algo como esto?


No quiero crear una enumeración estática generada por código (según el artículo de The Code Project Generador de código de enumeración: generación de código de enumeración automáticamente desde las tablas de búsqueda de la base de datos ) y preferiría que fuera completamente automático.

billfredtom
fuente
¿Sería posible que esté intentando utilizar una enumeración de una forma en la que haya una mejor solución?
Dan
Estoy con @Dan, tiene que haber una mejor manera de hacer esto.
N_A
@mydogisbox ¿cuál es la mejor manera?
eran otzap
@eranotzer En realidad, después de pensarlo un poco, sería bastante simple escribir un paso previo a la compilación que consulte la base de datos y genere una enumeración a partir de ella
N_A
1
Dicho esto, no estoy seguro de lo que quiere decir con "No quiero crear una enumeración estática generada por código", así que tal vez esto no se ajuste a la necesidad.
N_A

Respuestas:

97

Estoy haciendo exactamente esto, pero necesitas hacer algún tipo de generación de código para que esto funcione.

En mi solución, agregué un proyecto "EnumeratedTypes". Esta es una aplicación de consola que obtiene todos los valores de la base de datos y construye las enumeraciones a partir de ellos. Luego, guarda todas las enumeraciones en un ensamblado.

El código de generación de enumeración es así:

// Get the current application domain for the current thread
AppDomain currentDomain = AppDomain.CurrentDomain;

// Create a dynamic assembly in the current application domain,
// and allow it to be executed and saved to disk.
AssemblyName name = new AssemblyName("MyEnums");
AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(name,
                                      AssemblyBuilderAccess.RunAndSave);

// Define a dynamic module in "MyEnums" assembly.
// For a single-module assembly, the module has the same name as the assembly.
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name.Name,
                                  name.Name + ".dll");

// Define a public enumeration with the name "MyEnum" and an underlying type of Integer.
EnumBuilder myEnum = moduleBuilder.DefineEnum("EnumeratedTypes.MyEnum",
                         TypeAttributes.Public, typeof(int));

// Get data from database
MyDataAdapter someAdapter = new MyDataAdapter();
MyDataSet.MyDataTable myData = myDataAdapter.GetMyData();

foreach (MyDataSet.MyDataRow row in myData.Rows)
{
    myEnum.DefineLiteral(row.Name, row.Key);
}

// Create the enum
myEnum.CreateType();

// Finally, save the assembly
assemblyBuilder.Save(name.Name + ".dll");

Mis otros proyectos en la solución hacen referencia a este ensamblado generado. Como resultado, puedo usar las enumeraciones dinámicas en el código, completo con intellisense.

Luego, agregué un evento posterior a la compilación para que después de compilar este proyecto "EnumeratedTypes", se ejecute solo y genere el archivo "MyEnums.dll".

Por cierto, es útil cambiar el orden de construcción de su proyecto para que "EnumeratedTypes" se compile primero. De lo contrario, una vez que comience a usar su .dll generado dinámicamente, no podrá hacer una compilación si el .dll alguna vez se elimina. (Problema de la gallina y el huevo: sus otros proyectos en la solución necesitan este .dll para compilarse correctamente, y no puede crear el .dll hasta que cree su solución ...)

Obtuve la mayor parte del código anterior de Obtuve este artículo de msdn .

¡Espero que esto ayude!

Pandincus
fuente
7
Para aquellos que no saben cómo ejecutar el ejecutable resultante después de la compilación: 1) Haga clic con el botón derecho en el proyecto 2) Haga clic en propiedades 3) Haga clic en Eventos de compilación 4) En el cuadro de texto "Líneas de comando de eventos posteriores a la compilación", escriba $ (TargetPath)
Miguel
¿Es posible hacer Dynamic Enum con una definición de atributo personalizada como se menciona en este enlace ?
Balagurunathan Marimuthu
49

Las enumeraciones deben especificarse en tiempo de compilación, no puede agregar dinámicamente enumeraciones durante el tiempo de ejecución, y ¿por qué lo haría? ¿No habría uso / referencia a ellas en el código?

Desde Professional C # 2008:

El verdadero poder de las enumeraciones en C # es que, en segundo plano, se crean instancias como estructuras derivadas de la clase base, System.Enum. Esto significa que es posible llamar a métodos contra ellos para realizar algunas tareas útiles. Tenga en cuenta que debido a la forma en que se implementa .NET Framework, no hay pérdida de rendimiento asociada con el tratamiento sintáctico de las enumeraciones como estructuras. En la práctica, una vez compilado el código, las enumeraciones existirán como tipos primitivos, al igual que int y float.

Por lo tanto, no estoy seguro de que pueda usar Enums de la manera que desee.

Marcus L
fuente
1
No estoy seguro de cuál es el razonamiento de billfredtom, pero el mío era que podía evitar hacer búsquedas manuales de cadenas para ciertas claves, en lugar de tenerlas integradas en mi código. Simplemente prefiero poder realizar lógica en valores fuertemente tipados en lugar de cadenas débiles. Una advertencia sería que, dado que ahora tenemos código que se basa en un Enum generado dinámicamente, si eliminamos el valor de la base de datos, la próxima vez que intentemos compilar nuestro código fallará.
Pandincus
14
El cartel y los 18 votos a favor perdieron un poco su punto. Parece que quiere enumeraciones generadas , no enumeraciones dinámicas en tiempo de ejecución.
Matt Mitchell
+1. Una enumeración es básicamente otra forma de definir constantes enteras (incluso si System.Enumtiene alguna funcionalidad adicional). En lugar de escribir const int Red=0, Green=1, Blue=3;, escribes enum { Red, Green, Blue }. Una constante es por definición constante y no dinámica.
Olivier Jacot-Descombes
2
@Oliver Si quieres discutir semántica, sí, tienes razón. Pero estoy de acuerdo con el comentario de Graphain: creo que el OP está buscando enumeraciones generadas . Quiere que los valores de enumeración provengan de la base de datos y no tener que codificarlos.
Pandincus
1
O ... digamos que permito que alguien en mi web.config defina tipos de tokens para plantillas de correo electrónico para mi código de plantillas de correo electrónico. Sería bueno si mi enumeración existente llamada EmailTokens que representa esos tipos de cadenas se generara en función de los tipos definidos en mi web.config. Entonces, si alguien agrega un nuevo token de correo electrónico en el webconfig a través de mi valor clave, por ejemplo, "Email, FName" y ya tengo una enumeración que voy a usar para representar estos tokens, como EmailTemplate. agregue un nuevo token de cadena en esa clave en web.config y mi enumeración agregaría automáticamente la const
PositiveGuy
18

¿Tiene que ser una enumeración real? ¿Qué tal usar un Dictionary<string,int>en su lugar?

por ejemplo

Dictionary<string, int> MyEnum = new Dictionary(){{"One", 1}, {"Two", 2}};
Console.WriteLine(MyEnum["One"]);
Autodidacta
fuente
11
No trataría de hacerlo de esta manera. Pierde sus comprobaciones de tiempo de compilación y se vuelve propenso a errores tipográficos. Todos los beneficios de las enumeraciones desaparecieron. Puede introducir constantes de cadena, pero luego está de vuelta donde comenzó.
Daniel Brückner
1
Estoy de acuerdo. Pero recuerde que las cadenas mal escritas se detectarán en tiempo de ejecución. Simplemente agregue un caso de prueba para cubrir todos los miembros de la enumeración.
Autodidacta
1
escribir mal no es un problema si usa constantes en lugar de literales
Maslow
@Maslow Suponga que se refiere a enumeraciones, no a constantes de cadena.
Matt Mitchell
4
+1. El uso de un diccionario o un HashSet se acerca más a lo que podría ser una enumeración dinámica. Completamente dinámico significa que ocurre en tiempo de ejecución y, por lo tanto, la verificación de errores tendrá que ocurrir en tiempo de ejecución.
Olivier Jacot-Descombes
13

Hice esto con una plantilla T4 . Es bastante trivial colocar un archivo .tt en su proyecto y configurar Visual Studio para ejecutar la plantilla T4 como un paso previo a la compilación.

El T4 genera un archivo .cs, lo que significa que puede hacer que solo consulte la base de datos y cree una enumeración en un archivo .cs a partir del resultado. Conectado como una tarea previa a la compilación, volvería a crear su enumeración en cada compilación, o puede ejecutar el T4 manualmente según sea necesario.

Codificación con espiga
fuente
12

Digamos que tiene lo siguiente en su base de datos:

table enums
-----------------
| id | name     |
-----------------
| 0  | MyEnum   |
| 1  | YourEnum |
-----------------

table enum_values
----------------------------------
| id | enums_id | value | key    |
----------------------------------
| 0  | 0        | 0     | Apple  |
| 1  | 0        | 1     | Banana |
| 2  | 0        | 2     | Pear   |
| 3  | 0        | 3     | Cherry |
| 4  | 1        | 0     | Red    |
| 5  | 1        | 1     | Green  |
| 6  | 1        | 2     | Yellow |
----------------------------------

Construya una selección para obtener los valores que necesita:

select * from enums e inner join enum_values ev on ev.enums_id=e.id where e.id=0

Construya el código fuente de la enumeración y obtendrá algo como:

String enumSourceCode = "enum " + enumName + "{" + enumKey1 + "=" enumValue1 + "," + enumKey2 + ... + "}";

(obviamente, esto se construye en un bucle de algún tipo).

Luego viene la parte divertida, compilar su enumeración y usarla:

CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters cs = new CompilerParameters();
cp.GenerateInMemory = True;

CompilerResult result = provider.CompileAssemblyFromSource(cp, enumSourceCode);

Type enumType = result.CompiledAssembly.GetType(enumName);

Ahora tiene el tipo compilado y listo para usar.
Para obtener un valor de enumeración almacenado en la base de datos, puede usar:

[Enum].Parse(enumType, value);

donde valor puede ser el valor entero (0, 1, etc.) o el texto / clave de enumeración (Apple, Banana, etc.)

Sani Singh Huttunen
fuente
4
¿De qué manera esto ayudaría realmente? No hay seguridad de tipo ni intellisense. Básicamente, es solo una forma más complicada de usar una constante, ya que él tiene que proporcionar el valor de todos modos.
Runeborg
2
Sani - ¡perfecto! Eso era exactamente lo que necesitaba. Para aquellos que cuestionan el motivo de algo como esto, estoy usando una biblioteca de proveedor que requiere que una propiedad se establezca con el nombre de una enumeración. La enumeración restringe el rango de valores válidos para una propiedad diferente del mismo objeto. En mi caso, estoy cargando metadatos, incluido el rango de valores válidos de una base de datos; y no, el código de proveedor no admite la transferencia de una colección de ningún tipo a la propiedad. Gracias
10

Solo mostrando la respuesta de Pandincus con el código "del estante" y alguna explicación: Necesitas dos soluciones para este ejemplo (sé que también se podría hacer a través de una;), deja que los estudiantes avanzados la presenten ...

Entonces aquí está el SQL DDL para la tabla:

USE [ocms_dev]
    GO

CREATE TABLE [dbo].[Role](
    [RoleId] [int] IDENTITY(1,1) NOT NULL,
    [RoleName] [varchar](50) NULL
) ON [PRIMARY]

Así que aquí está el programa de consola que produce el dll:

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Data.Common;
using System.Data;
using System.Data.SqlClient;

namespace DynamicEnums
{
    class EnumCreator
    {
        // after running for first time rename this method to Main1
        static void Main ()
        {
            string strAssemblyName = "MyEnums";
            bool flagFileExists = System.IO.File.Exists (
                   AppDomain.CurrentDomain.SetupInformation.ApplicationBase + 
                   strAssemblyName + ".dll"
            );

            // Get the current application domain for the current thread
            AppDomain currentDomain = AppDomain.CurrentDomain;

            // Create a dynamic assembly in the current application domain,
            // and allow it to be executed and saved to disk.
            AssemblyName name = new AssemblyName ( strAssemblyName );
            AssemblyBuilder assemblyBuilder = 
                    currentDomain.DefineDynamicAssembly ( name,
                            AssemblyBuilderAccess.RunAndSave );

            // Define a dynamic module in "MyEnums" assembly.
            // For a single-module assembly, the module has the same name as
            // the assembly.
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule (
                    name.Name, name.Name + ".dll" );

            // Define a public enumeration with the name "MyEnum" and
            // an underlying type of Integer.
            EnumBuilder myEnum = moduleBuilder.DefineEnum (
                    "EnumeratedTypes.MyEnum",
                    TypeAttributes.Public,
                    typeof ( int )
            );

            #region GetTheDataFromTheDatabase
            DataTable tableData = new DataTable ( "enumSourceDataTable" );

            string connectionString = "Integrated Security=SSPI;Persist " +
                    "Security Info=False;Initial Catalog=ocms_dev;Data " +
                    "Source=ysg";

            using (SqlConnection connection = 
                    new SqlConnection ( connectionString ))
            {

                SqlCommand command = connection.CreateCommand ();
                command.CommandText = string.Format ( "SELECT [RoleId], " + 
                        "[RoleName] FROM [ocms_dev].[dbo].[Role]" );

                Console.WriteLine ( "command.CommandText is " + 
                        command.CommandText );

                connection.Open ();
                tableData.Load ( command.ExecuteReader ( 
                        CommandBehavior.CloseConnection
                ) );
            } //eof using

            foreach (DataRow dr in tableData.Rows)
            {
                myEnum.DefineLiteral ( dr[1].ToString (),
                        Convert.ToInt32 ( dr[0].ToString () ) );
            }
            #endregion GetTheDataFromTheDatabase

            // Create the enum
            myEnum.CreateType ();

            // Finally, save the assembly
            assemblyBuilder.Save ( name.Name + ".dll" );
        } //eof Main 
    } //eof Program
} //eof namespace 

Aquí está la programación de la consola imprimiendo la salida (recuerde que tiene que hacer referencia a la dll). Deje que los estudiantes avanzados presenten la solución para combinar todo en una solución con carga dinámica y verificar si ya existe una dll de compilación.

// add the reference to the newly generated dll
use MyEnums ; 

class Program
{
    static void Main ()
    {
        Array values = Enum.GetValues ( typeof ( EnumeratedTypes.MyEnum ) );

        foreach (EnumeratedTypes.MyEnum val in values)
        {
            Console.WriteLine ( String.Format ( "{0}: {1}",
                    Enum.GetName ( typeof ( EnumeratedTypes.MyEnum ), val ),
                    val ) );
        }

        Console.WriteLine ( "Hit enter to exit " );
        Console.ReadLine ();
    } //eof Main 
} //eof Program
Yordan Georgiev
fuente
1
@YordanGeorgiev -¿Por qué declara flagFileExistscuando no se usa en ningún otro lugar de la aplicación?
Michael Kniskern
2
Supongo que es un error que; I)
Yordan Georgiev
5

¿No estamos llegando a esto desde la dirección equivocada?

Si es probable que los datos cambien durante la vida útil de la versión implementada, entonces una enumeración simplemente no es apropiada y debe usar un diccionario, hash u otra colección dinámica.

Si sabe que el conjunto de valores posibles está fijo durante la vida útil de la versión implementada, es preferible una enumeración.

Si debe tener algo en su base de datos que replique el conjunto enumerado, ¿por qué no agregar un paso de implementación para borrar y volver a llenar la tabla de la base de datos con el conjunto definitivo de valores de enumeración?

Brian Lowe
fuente
Sí y no, sí, porque tienes razón, todo el punto es que la enumeración es estática. Puede evitar errores tipográficos y también saber qué hay disponible. Con dictionary y db, podría ser cualquier cosa. Pero a veces desea la fruta de ambos árboles cuando solo se le permite recoger de uno.
Ken
4

Siempre me gusta escribir mi propia "enumeración personalizada". De lo que tengo una clase que es un poco más compleja, pero puedo reutilizarla:

public abstract class CustomEnum
{
    private readonly string _name;
    private readonly object _id;

    protected CustomEnum( string name, object id )
    {
        _name = name;
        _id = id;
    }

    public string Name
    {
        get { return _name; }
    }

    public object Id
    {
        get { return _id; }
    }

    public override string ToString()
    {
        return _name;
    }
}

public abstract class CustomEnum<TEnumType, TIdType> : CustomEnum
    where TEnumType : CustomEnum<TEnumType, TIdType>
{
    protected CustomEnum( string name, TIdType id )
        : base( name, id )
    { }

    public new TIdType Id
    {
        get { return (TIdType)base.Id; }
    }

    public static TEnumType FromName( string name )
    {
        try
        {
            return FromDelegate( entry => entry.Name.Equals( name ) );
        }
        catch (ArgumentException ae)
        {
            throw new ArgumentException( "Illegal name for custom enum '" + typeof( TEnumType ).Name + "'", ae );
        }
    }

    public static TEnumType FromId( TIdType id )
    {
        try
        {
            return FromDelegate( entry => entry.Id.Equals( id ) );
        }
        catch (ArgumentException ae)
        {
            throw new ArgumentException( "Illegal id for custom enum '" + typeof( TEnumType ).Name + "'", ae );
        }
    }

    public static IEnumerable<TEnumType> GetAll()
    {
        var elements = new Collection<TEnumType>();
        var infoArray = typeof( TEnumType ).GetFields( BindingFlags.Public | BindingFlags.Static );

        foreach (var info in infoArray)
        {
            var type = info.GetValue( null ) as TEnumType;
            elements.Add( type );
        }

        return elements;
    }

    protected static TEnumType FromDelegate( Predicate<TEnumType> predicate )
    {
        if(predicate == null)
            throw new ArgumentNullException( "predicate" );

        foreach (var entry in GetAll())
        {
            if (predicate( entry ))
                return entry;
        }

        throw new ArgumentException( "Element not found while using predicate" );
    }
}

Ahora solo necesito crear mi enumeración que quiero usar:

 public sealed class SampleEnum : CustomEnum<SampleEnum, int>
    {
        public static readonly SampleEnum Element1 = new SampleEnum( "Element1", 1, "foo" );
        public static readonly SampleEnum Element2 = new SampleEnum( "Element2", 2, "bar" );

        private SampleEnum( string name, int id, string additionalText )
            : base( name, id )
        {
            AdditionalText = additionalText;
        }

        public string AdditionalText { get; private set; }
    }

Por fin puedo usarlo como quiero:

 static void Main( string[] args )
        {
            foreach (var element in SampleEnum.GetAll())
            {
                Console.WriteLine( "{0}: {1}", element, element.AdditionalText );
                Console.WriteLine( "Is 'Element2': {0}", element == SampleEnum.Element2 );
                Console.WriteLine();
            }

            Console.ReadKey();
        }

Y mi salida sería:

Element1: foo
Is 'Element2': False

Element2: bar
Is 'Element2': True    
Traummaennlein
fuente
2

Quieres System.Web.Compilation.BuildProvider

También dudo de la sabiduría de hacer esto, pero tal vez haya un buen caso de uso en el que no puedo pensar.

Lo que está buscando son proveedores de compilación, es decir, System.Web.Compilation.BuildProvider

SubSonic los usa de manera muy efectiva , puede descargar la fuente y ver cómo los usan, no necesitará nada ni la mitad de complicado de lo que están haciendo.

Espero que esto ayude.

Preocupado binario
fuente
0

No creo que haya una buena forma de hacer lo que quieres. Y si lo piensas, no creo que esto sea lo que realmente quieres.

Si tuviera una enumeración dinámica, también significa que debe alimentarla con un valor dinámico cuando haga referencia a ella. Tal vez con mucha magia podría lograr algún tipo de IntelliSense que se encargaría de esto y generaría una enumeración para usted en un archivo DLL. Pero considere la cantidad de trabajo que tomaría, lo poco efectivo que sería acceder a la base de datos para obtener información de IntelliSense, así como la pesadilla de la versión que controla el archivo DLL generado.

Si realmente no desea agregar manualmente los valores de enumeración (tendrá que agregarlos a la base de datos de todos modos), use una herramienta de generación de código, por ejemplo, plantillas T4 . Haga clic derecho + ejecutar y obtendrá su enumeración definida estáticamente en el código y obtendrá todos los beneficios de usar enumeraciones.

Runeborg
fuente
0

El uso de enumeraciones dinámicas es malo sin importar de qué manera. Tendrá que tomarse la molestia de "duplicar" los datos para garantizar un código claro y fácil de mantener en el futuro.

Si comienza a introducir bibliotecas generadas automáticamente, seguramente está causando más confusión a los futuros desarrolladores que tengan que actualizar su código que simplemente hacer que su enumeración esté codificada dentro del objeto de clase apropiado.

Los otros ejemplos dados suenan agradables y emocionantes, pero piense en la sobrecarga del mantenimiento del código en comparación con lo que obtiene de él. Además, ¿esos valores van a cambiar con tanta frecuencia?

Martín
fuente
0

Una forma de mantener las enumeraciones y crear una lista dinámica de valores al mismo tiempo es utilizar las enumeraciones que tiene actualmente con un diccionario creado dinámicamente.

Dado que la mayoría de las enumeraciones se utilizan en el contexto en el que están definidas para su uso, y las "enumeraciones dinámicas" serán compatibles con procesos dinámicos, puede distinguir el 2.

El primer paso es crear una tabla / colección que contenga los ID y las referencias de las entradas dinámicas. En la tabla, el autoincremento será mucho mayor que el valor de enumeración más grande.

Ahora viene la parte de sus Enums dinámicos, supongo que usará los Enums para crear un conjunto de condiciones que aplican un conjunto de reglas, algunas se generan dinámicamente.

Get integer from database
If Integer is in Enum -> create Enum -> then run Enum parts
If Integer is not a Enum -> create Dictionary from Table -> then run Dictionary parts.
Jackstine
fuente
0

clase de constructor de enumeración

public class XEnum
{
    private EnumBuilder enumBuilder;
    private int index;
    private AssemblyBuilder _ab;
    private AssemblyName _name;
    public XEnum(string enumname)
    {
        AppDomain currentDomain = AppDomain.CurrentDomain;
        _name = new AssemblyName("MyAssembly");
        _ab = currentDomain.DefineDynamicAssembly(
            _name, AssemblyBuilderAccess.RunAndSave);

        ModuleBuilder mb = _ab.DefineDynamicModule("MyModule");

        enumBuilder = mb.DefineEnum(enumname, TypeAttributes.Public, typeof(int));


    }
    /// <summary>
    /// adding one string to enum
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    public FieldBuilder add(string s)
    {
        FieldBuilder f = enumBuilder.DefineLiteral(s, index);
        index++;
        return f;
    }
    /// <summary>
    /// adding array to enum
    /// </summary>
    /// <param name="s"></param>
    public void addRange(string[] s)
    {
        for (int i = 0; i < s.Length; i++)
        {
            enumBuilder.DefineLiteral(s[i], i);
        }
    }
    /// <summary>
    /// getting index 0
    /// </summary>
    /// <returns></returns>
    public object getEnum()
    {
        Type finished = enumBuilder.CreateType();
        _ab.Save(_name.Name + ".dll");
        Object o1 = Enum.Parse(finished, "0");
        return o1;
    }
    /// <summary>
    /// getting with index
    /// </summary>
    /// <param name="i"></param>
    /// <returns></returns>
    public object getEnum(int i)
    {
        Type finished = enumBuilder.CreateType();
        _ab.Save(_name.Name + ".dll");
        Object o1 = Enum.Parse(finished, i.ToString());
        return o1;
    }
}

crear un objeto

string[] types = { "String", "Boolean", "Int32", "Enum", "Point", "Thickness", "long", "float" };
XEnum xe = new XEnum("Enum");
        xe.addRange(types);
        return xe.getEnum();
Mahdi Khalili
fuente