Criar automaticamente um Enum com base em valores em uma tabela de pesquisa de banco de dados?

116

Como faço para criar automaticamente um enum e, subsequentemente, usar seus valores em C # com base em valores em uma tabela de pesquisa de banco de dados (usando a camada de dados da biblioteca corporativa)?

Por exemplo, se eu adicionar um novo valor de pesquisa no banco de dados, não quero ter que adicionar manualmente a declaração de valor enum estático extra no código - gostaria de manter o enum sincronizado com o banco de dados.

Existe algo assim?


Não quero criar um enum estático gerado por código (de acordo com o artigo Enum Code Generator do The Code Project - Gerando código enum automaticamente a partir de tabelas de pesquisa de banco de dados ) e preferiria que fosse totalmente automático.

Billfredtom
fonte
Seria possível que você esteja tentando usar uma enumeração de uma forma em que haja uma solução melhor?
Dan,
Estou com @Dan, tem que haver uma maneira melhor de fazer isso.
N_A
@mydogisbox qual a melhor maneira?
Eran Otzap
@eranotzer Na verdade, depois de pensar um pouco sobre isso, seria muito simples escrever uma etapa de pré-construção que consulta o banco de dados e gera um enum a partir dele
N_A
1
Dito isso, não tenho certeza do que ele quer dizer com "Não quero criar um enum estático gerado por código", então talvez isso não seja adequado.
N_A

Respostas:

97

Estou fazendo exatamente isso, mas você precisa fazer algum tipo de geração de código para que isso funcione.

Em minha solução, adicionei um projeto "EnumeratedTypes". Este é um aplicativo de console que obtém todos os valores do banco de dados e constrói os enums a partir deles. Em seguida, ele salva todos os enums em uma montagem.

O código de geração de enum é assim:

// 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");

Meus outros projetos na solução fazem referência a esse assembly gerado. Como resultado, posso usar os enums dinâmicos no código, completos com intellisense.

Em seguida, adicionei um evento pós-compilação para que, após a construção deste projeto "EnumeratedTypes", ele execute a si mesmo e gere o arquivo "MyEnums.dll".

A propósito, ajuda a alterar a ordem de construção de seu projeto para que "EnumeratedTypes" seja construído primeiro. Caso contrário, depois de começar a usar seu .dll gerado dinamicamente, você não conseguirá fazer uma compilação se o .dll for excluído. (O tipo de problema do ovo e da galinha - seus outros projetos na solução precisam desse .dll para ser compilado corretamente, e você não pode criar o .dll até criar sua solução ...)

Peguei a maior parte do código acima neste artigo do msdn .

Espero que isto ajude!

Pandincus
fonte
7
Para quem não sabe como executar o executável resultante na pós-compilação: 1) Clique com o botão direito do mouse no projeto 2) Clique nas propriedades 3) Clique em Eventos de compilação 4) Na caixa de texto "Linhas de comando do evento pós-compilação" $ (TargetPath)
Miguel
É possível fazer Enum Dinâmico com definição de atributo personalizado conforme mencionado neste link ?
Balagurunathan Marimuthu,
49

Os enums devem ser especificados em tempo de compilação, você não pode adicionar enums dinamicamente durante o tempo de execução - e por que faria isso, não haveria uso / referência a eles no código?

Do Professional C # 2008:

O verdadeiro poder dos enums em C # é que, nos bastidores, eles são instanciados como estruturas derivadas da classe base, System.Enum. Isso significa que é possível chamar métodos contra eles para realizar algumas tarefas úteis. Observe que, devido à maneira como o .NET Framework é implementado, não há perda de desempenho associada ao tratamento das enums sintaticamente como estruturas. Na prática, uma vez que seu código é compilado, enums existirão como tipos primitivos, assim como int e float.

Portanto, não tenho certeza se você pode usar Enums da maneira que deseja.

Marcus L
fonte
1
não tenho certeza de qual é o raciocínio de billfredtom, mas o meu era que eu poderia evitar fazer pesquisas manuais de strings para certas chaves, em vez de tê-las embutidas em meu código. Eu apenas prefiro ser capaz de executar lógica em valores fortemente tipados em vez de strings fracas. Uma ressalva seria que, uma vez que agora temos um código que depende de um Enum gerado dinamicamente, se excluirmos o valor do banco de dados, na próxima vez que tentarmos compilar nosso código, ele falhará.
Pandincus
14
Poster e 18 votos positivos não entenderam bem o que ele queria dizer. Parece que ele deseja enums gerados , não enums dinâmicos de tempo de execução.
Matt Mitchell
+1. Um enum é basicamente apenas outra maneira de definir constantes inteiras (mesmo que System.Enumtenha alguma funcionalidade adicional). Em vez de escrever, const int Red=0, Green=1, Blue=3;você escreve enum { Red, Green, Blue }. Uma constante é, por definição, constante e não dinâmica.
Olivier Jacot-Descombes
2
@Oliver Se você quiser discutir a semântica, sim, você está correto. Mas concordo com o comentário de Graphain - acredito que o OP está procurando enums gerados . Ele deseja que os valores enum venham do banco de dados e não precise codificá-los permanentemente.
Pandincus
1
Ou ... digamos que eu permita que alguém em meu web.config defina tipos de token para modelos de e-mail para meu código de modelo de e-mail. Seria bom se meu enum existente chamado EmailTokens, que representa esses tipos de string, fosse gerado com base nos tipos definidos em meu web.config. Portanto, se alguém adicionar um novo token de e-mail no webconfig por meio do meu valor-chave, por exemplo, "Email, FName" e eu já tiver um enum que irei usar para representar esses tokens, como EmailTemplate.Email, seria bom se alguém pudesse apenas adicione um novo token de string nessa chave no web.config e meu enum adicionaria automaticamente o const
PositiveGuy
18

Tem que ser um enum real? Que tal usar um Dictionary<string,int>?

por exemplo

Dictionary<string, int> MyEnum = new Dictionary(){{"One", 1}, {"Two", 2}};
Console.WriteLine(MyEnum["One"]);
Autodidata
fonte
11
Eu não tentaria fazer dessa maneira. Você perde suas verificações de tempo de compilação e fica sujeito a erros de digitação. Todos os benefícios dos enums se foram. Você poderia introduzir constantes de string, mas depois estará de volta ao ponto de partida.
Daniel Brückner,
1
Concordo. Mas lembre-se de que as strings digitadas incorretamente serão capturadas no tempo de execução. Basta adicionar um caso de teste para cobrir todos os membros do enum.
Autodidata,
1
erros de digitação não são um problema se você usar constantes em vez de literais
Maslow
@Maslow Suponha que você queira dizer enums, não constantes de string.
Matt Mitchell
4
+1. Usar um dicionário ou um HashSet chega mais perto do que poderia ser um enum dinâmico. Totalmente dinâmico significa que isso acontece em tempo de execução e, portanto, a verificação de erros terá que ocorrer em tempo de execução.
Olivier Jacot-Descombes
13

Eu fiz isso com um modelo T4 . É bastante trivial colocar um arquivo .tt em seu projeto e configurar o Visual Studio para executar o modelo T4 como uma etapa de pré-construção.

O T4 gera um arquivo .cs, o que significa que você pode fazer com que ele apenas consulte o banco de dados e crie um enum em um arquivo .cs a partir do resultado. Conectado como uma tarefa de pré-compilação, ele recriaria seu enum em cada compilação, ou você pode executar o T4 manualmente conforme necessário.

CodingWithSpike
fonte
12

Digamos que você tenha o seguinte em seu banco de dados:

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 |
----------------------------------

Construa uma seleção para obter os valores de que você precisa:

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

Construa o código-fonte para o enum e você obterá algo como:

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

(obviamente, isso é construído em algum tipo de loop.)

Em seguida, vem a parte divertida, Compilar seu enum e usá-lo:

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

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

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

Agora você tem o tipo compilado e pronto para uso.
Para obter um valor enum armazenado no banco de dados, você pode usar:

[Enum].Parse(enumType, value);

onde o valor pode ser o valor inteiro (0, 1, etc.) ou o texto / chave enum (Apple, Banana, etc.)

Sani Singh Huttunen
fonte
4
De que forma isso realmente ajudaria? Não há segurança de tipo e intelisense. Basicamente, é apenas uma maneira mais complicada de usar uma constante, pois ele precisa fornecer o valor de qualquer maneira.
Runeborg,
2
Sani - perfeito! Isso era exatamente o que eu precisava. Para aqueles que questionam o motivo de algo assim, estou usando uma biblioteca de fornecedores que exige que uma propriedade seja definida como o nome de uma enumeração. A enumeração restringe o intervalo de valores válidos para uma propriedade diferente do mesmo objeto. No meu caso, estou carregando metadados, incluindo o intervalo de valor válido de um banco de dados; e não, o código do fornecedor não suporta a passagem de uma coleção de qualquer tipo para a propriedade. Obrigado
10

Apenas mostrando a resposta do Pandincus com o código "da prateleira" e alguma explicação: Você precisa de duas soluções para este exemplo (eu sei que também poderia ser feito por meio de uma;), deixe os alunos avançados apresentá-la ...

Então, aqui está o DDL SQL para a tabela:

USE [ocms_dev]
    GO

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

Então, aqui está o programa do console produzindo a 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 

Aqui está a programação do console imprimindo a saída (lembre-se de que ela deve fazer referência à dll). Deixe os alunos avançados apresentarem a solução para combinar tudo em uma solução com carregamento dinâmico e verificar se já existe uma DLL construída.

// 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
fonte
1
@YordanGeorgiev -Por que você declara flagFileExistsquando não é usado em nenhum outro lugar do aplicativo?
Michael Kniskern
2
Eu acho que é um bug do que; I)
Yordan Georgiev
5

Não estamos vindo da direção errada?

Se houver probabilidade de alteração dos dados durante o tempo de vida da versão implantada, um enum simplesmente não é apropriado e você precisa usar um dicionário, hash ou outra coleção dinâmica.

Se você souber que o conjunto de valores possíveis é fixo durante a vida útil da versão implantada, um enum é preferível.

Se você deve ter algo em seu banco de dados que replique o conjunto enumerado, por que não adicionar uma etapa de implantação para limpar e preencher novamente a tabela de banco de dados com o conjunto definitivo de valores enum?

Brian Lowe
fonte
Sim e não, sim, porque você está correto, todo o ponto é enum é estático. Você pode evitar erros de digitação e também saber o que está disponível. Com dicionário e db - pode ser qualquer coisa. Mas às vezes você quer os frutos de ambas as árvores, quando só pode colher de uma.
Ken
4

Eu sempre gosto de escrever meu próprio "enum personalizado". Então, tenho uma classe um pouco mais complexa, mas posso reutilizá-la:

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" );
    }
}

Agora só preciso criar meu enum que desejo 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; }
    }

Enfim posso usá-lo como quiser:

 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();
        }

E minha saída seria:

Element1: foo
Is 'Element2': False

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

Você deseja System.Web.Compilation.BuildProvider

Também duvido da sensatez de fazer isso, mas talvez haja um bom caso de uso que eu não consigo pensar.

O que você está procurando são provedores de compilação, ou seja, System.Web.Compilation.BuildProvider

Eles são usados ​​de forma muito eficaz pelo SubSonic , você pode baixar o código-fonte e ver como eles os usam, você não vai precisar de nada tão complicado quanto o que eles estão fazendo.

Espero que isto ajude.

Preocupante binário
fonte
0

Não acho que haja uma boa maneira de fazer o que você quer. E se você pensar bem, não acho que é isso que você realmente quer.

Se você tiver um enum dinâmico, também significa que deve alimentá-lo com um valor dinâmico ao fazer referência a ele. Talvez com muita magia você pudesse obter algum tipo de IntelliSense que cuidaria disso e geraria um enum para você em um arquivo DLL. Mas considere a quantidade de trabalho que seria necessária, o quão ineficaz seria acessar o banco de dados para buscar informações do IntelliSense, bem como o pesadelo da versão controlando o arquivo DLL gerado.

Se você realmente não deseja adicionar manualmente os valores enum (você terá que adicioná-los ao banco de dados de qualquer maneira), use uma ferramenta de geração de código, por exemplo, modelos T4 . Clique com o botão direito + execute e você terá seu enum definido estaticamente no código e terá todos os benefícios de usar enums.

Runeborg
fonte
0

Usar enums dinâmicos é ruim, não importa o caminho. Você terá que se dar ao trabalho de "duplicar" os dados para garantir um código claro e fácil de manter no futuro.

Se você começar a introduzir bibliotecas geradas automaticamente, com certeza causará mais confusão para futuros desenvolvedores, tendo que atualizar seu código do que simplesmente codificar seu enum dentro do objeto de classe apropriado.

Os outros exemplos dados parecem bons e empolgantes, mas pense sobre a sobrecarga na manutenção do código em comparação com o que você obtém com isso. Além disso, esses valores vão mudar com tanta frequência?

Martin
fonte
0

Uma maneira de manter os Enums e criar uma lista dinâmica de valores ao mesmo tempo é usar os Enums que você tem atualmente com um Dicionário criado dinamicamente.

Como a maioria dos Enums são usados ​​no contexto em que foram definidos para serem usados ​​e os "enums dinâmicos" serão suportados por processos dinâmicos, você pode distinguir os 2.

A primeira etapa é criar uma tabela / coleção que abrigue os IDs e referências para as entradas dinâmicas. Na tabela, você fará um incremento automático muito maior do que seu maior valor Enum.

Agora vem a parte de seus Enums dinâmicos, estou assumindo que você usará os Enums para criar um conjunto de condições que aplicam um conjunto de regras, algumas são geradas dinamicamente.

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
fonte
0

classe construtor enum

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;
    }
}

criar um objeto

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