Автоматически создавать Enum на основе значений в таблице поиска в базе данных?
Как автоматически создать перечисление и впоследствии использовать его значения в C# на основе значений в таблице поиска в базе данных (с использованием уровня данных корпоративной библиотеки)?
Например, если я добавлю новое значение поиска в базу данных, мне не нужно будет вручную добавлять в код дополнительное объявление статического значения перечисления - я бы хотел синхронизировать перечисление с базой данных.
Есть ли такая вещь, как эта?
Я не хочу создавать сгенерированный кодом статический enum (согласно статье Project Code Enum Code Generator - Автоматическое создание кода enum из таблиц поиска в базе данных) и предпочел бы, чтобы он был полностью автоматическим.
14 ответов
Я делаю именно это, но вам нужно сделать какое-то генерирование кода, чтобы это работало.
В моём решении я добавил проект "EnumeratedTypes". Это консольное приложение, которое получает все значения из базы данных и создает из них перечисления. Затем он сохраняет все перечисления в сборке.
Код генерации перечисления выглядит так:
// 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");
Мои другие проекты в решении ссылаются на эту сгенерированную сборку. В результате я могу использовать динамические перечисления в коде, дополненные intellisense.
Затем я добавил событие после сборки, чтобы после создания проекта "EnumeratedTypes" он запускался сам и генерировал файл "MyEnums.dll".
Между прочим, это помогает изменить порядок сборки вашего проекта, так что "EnumeratedTypes" создается в первую очередь. В противном случае, как только вы начнете использовать динамически сгенерированный DLL-файл, вы не сможете выполнить сборку, если DLL-файл когда-либо будет удален. (Проблема с курицей и яйцом - вашим другим проектам в решении нужен этот.dll для правильной сборки, и вы не можете создать.dll, пока не создадите свое решение...)
Я получил большую часть приведенного выше кода из этой статьи MSDN.
Надеюсь это поможет!
Перечисления должны быть указаны во время компиляции, вы не можете динамически добавлять перечисления во время выполнения - и почему бы вам не использовать / ссылаться на них в коде?
Из Профессиональной C# 2008:
Реальная сила перечислений в C# заключается в том, что за кулисами они создаются как структуры, производные от базового класса System.Enum . Это означает, что можно вызывать методы против них для выполнения некоторых полезных задач. Обратите внимание, что из-за способа реализации.NET Framework нет потери производительности, связанной с синтаксической обработкой перечислений как структур. На практике, как только ваш код скомпилирован, перечисления будут существовать как примитивные типы, такие как int и float.
Так что я не уверен, что вы можете использовать Enums так, как хотите.
Должно ли это быть фактическим перечислением? Как насчет использования Dictionary<string,int>
вместо?
например
Dictionary<string, int> MyEnum = new Dictionary(){{"One", 1}, {"Two", 2}};
Console.WriteLine(MyEnum["One"]);
Я сделал это с помощью шаблона T4. Довольно просто вставить файл.tt в ваш проект и настроить Visual Studio для запуска шаблона T4 в качестве шага перед сборкой.
T4 генерирует файл.cs, что означает, что вы можете просто запросить базу данных и сформировать перечисление в файле.cs из результата. Смонтированный как задача перед сборкой, он будет пересоздавать ваше перечисление при каждой сборке, или вы можете вместо этого запускать T4 вручную.
Допустим, у вас есть следующее в вашей БД:
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 |
----------------------------------
Создайте выбор, чтобы получить нужные вам значения:
select * from enums e inner join enum_values ev on ev.enums_id=e.id where e.id=0
Создайте исходный код для перечисления, и вы получите что-то вроде:
String enumSourceCode = "enum " + enumName + "{" + enumKey1 + "=" enumValue1 + "," + enumKey2 + ... + "}";
(очевидно, это построено в петле некоторого вида.)
Затем начинается самое интересное, компилируя ваш enum и используя его:
CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters cs = new CompilerParameters();
cp.GenerateInMemory = True;
CompilerResult result = provider.CompileAssemblyFromSource(cp, enumSourceCode);
Type enumType = result.CompiledAssembly.GetType(enumName);
Теперь у вас есть тип, скомпилированный и готовый к использованию.
Чтобы получить значение enum, хранящееся в БД, вы можете использовать:
[Enum].Parse(enumType, value);
где значение может быть целочисленным значением (0, 1 и т. д.) или текстом / ключом перечисления (Apple, Banana и т. д.)
Просто показываю ответ Пандинкуса с кодом "полки" и некоторым объяснением: вам нужно два решения для этого примера (я знаю, что это можно сделать и с помощью одного;), пусть продвинутые студенты представят его...
Итак, вот DDL SQL для таблицы:
USE [ocms_dev]
GO
CREATE TABLE [dbo].[Role](
[RoleId] [int] IDENTITY(1,1) NOT NULL,
[RoleName] [varchar](50) NULL
) ON [PRIMARY]
Итак, вот консольная программа, производящая 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
Вот консольное программирование, печатающее вывод (помните, что оно должно ссылаться на dll). Предложите студентам-первокурсникам решение для объединения всего в одном решении с динамической загрузкой и проверки, если уже существует сборка dll.
// 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
Разве мы не подходим к этому с неправильной стороны?
Если данные могут измениться вообще в течение времени жизни развернутого выпуска, тогда перечисление просто не подходит, и вам нужно использовать словарь, хэш или другую динамическую коллекцию.
Если вы знаете, что набор возможных значений фиксирован в течение срока службы развернутого выпуска, тогда предпочтительным является перечисление.
Если в вашей базе данных есть что-то, что реплицирует перечислимый набор, то почему бы не добавить шаг развертывания, чтобы очистить и снова заполнить таблицу базы данных окончательным набором значений перечисления?
Я всегда люблю писать свои собственные "enum". Чем у меня есть один класс, который немного сложнее, но я могу использовать его повторно:
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" );
}
}
Теперь мне просто нужно создать свой enum, который я хочу использовать:
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; }
}
Наконец-то я могу использовать его так, как хочу:
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();
}
И мой вывод будет:
Element1: foo
Is 'Element2': False
Element2: bar
Is 'Element2': True
Вы хотите System.Web.Compilation.BuildProvider
Я также сомневаюсь в целесообразности этого, но, возможно, есть хороший вариант использования, о котором я не могу думать.
То, что вы ищете, являются поставщиками сборки, т.е. System.Web.Compilation.BuildProvider
Они очень эффективно используются SubSonic, вы можете скачать исходный код и посмотреть, как они их используют, вам не понадобится ничего более сложного, чем то, что они делают.
Надеюсь это поможет.
Послушайте, я тоже устал выписывать перечисления на основе столбцов таблицы Id / Name db, копировать и вставлять вещи из запросов в SSMS.
Ниже приведена очень грязная хранимая процедура, которая принимает в качестве входных данных имя таблицы, имя столбца, которое вы хотите использовать для имени перечисления C#, и имя столбца, которое вы хотите использовать для значения перечисления C#.
Большинство этих имен таблиц, с которыми я работаю, а) заканчиваются на "s" б) имеют столбец [TABLENAME]Id и в) имеют столбец [TABLENAME]Name, поэтому есть пара операторов if, которые будут предполагать эту структуру, в которой В этом случае параметры имени столбца не требуются.
Небольшой контекст для этих примеров - "Stonk" здесь на самом деле не означает "сток", но, как бы то ни было, то, как я использую "stonk", это означает "вещь, с которой связаны некоторые числа в течение определенного периода времени". Но это не важно, это просто пример таблицы с этой схемой Id / Name. Выглядит это так:
CREATE TABLE StonkTypes (
StonkTypeId TINYINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
StonkTypeName VARCHAR(200) NOT NULL CONSTRAINT UQ_StonkTypes_StonkTypeName UNIQUE (StonkTypeName)
)
После создания процедуры этот оператор:
EXEC CreateCSharpEnum 'StonkTypes'
Выбирает эту строку:
public enum StonkTypes { Stonk = 1, Bond = 2, Index = 3, Fund = 4, Commodity = 5,
PutCallRatio = 6, }
Который я могу скопировать и вставить в файл C#.
У меня есть таблица Stonks, в которой есть столбцы StonkId и StonkName, поэтому этот exec:
EXEC CreateCSharpEnum 'Stonks'
Выплевывает:
public enum Stonks { SP500 = 1, DowJonesIndustrialAverage = 2, ..... }
Но для этого перечисления я хочу использовать столбец "Символ" для значений имени перечисления, так что это:
EXEC CreateCSharpEnum 'Stonks', 'Symbol'
Уловка и рендеринг:
public enum Stonks { SPY = 1, DIA = 2, ..... }
Без лишних слов, вот этот грязный кусок безумия. Да, очень грязно, но я доволен собой - это код SQL, который создает код SQL, который создает код C#. Задействована пара слоев.
CREATE OR ALTER PROCEDURE CreateCSharpEnum
@TableName VARCHAR(MAX),
@EnumNameColumnName VARCHAR(MAX) = NULL,
@EnumValueColumnName VARCHAR(MAX) = NULL
AS
DECLARE @LastCharOfTableName VARCHAR(1)
SELECT @LastCharOfTableName = RIGHT(@TableName, 1)
PRINT 'Last char = [' + @LastCharOfTableName + ']'
DECLARE @TableNameWithoutS VARCHAR(MAX)
IF UPPER(@LastCharOfTableName) = 'S'
SET @TableNameWithoutS = LEFT(@TableName, LEN(@TableName) - 1)
ELSE
SET @TableNameWithoutS = @TableName
PRINT 'Table name without trailing s = [' + @TableNameWithoutS + ']'
IF @EnumNameColumnName IS NULL
BEGIN
SET @EnumNameColumnName = @TableNameWithoutS + 'Name'
END
PRINT 'name col name = [' + @EnumNameColumnName + ']'
IF @EnumValueColumnName IS NULL
SET @EnumValueColumnName = @TableNameWithoutS + 'Id'
PRINT 'value col name = [' + @EnumValueColumnName + ']'
-- replace spaces and punctuation
SET @EnumNameColumnName = 'REPLACE(' + @EnumNameColumnName + ', '' '', '''')'
SET @EnumNameColumnName = 'REPLACE(' + @EnumNameColumnName + ', ''&'', '''')'
SET @EnumNameColumnName = 'REPLACE(' + @EnumNameColumnName + ', ''.'', '''')'
SET @EnumNameColumnName = 'REPLACE(' + @EnumNameColumnName + ', ''('', '''')'
SET @EnumNameColumnName = 'REPLACE(' + @EnumNameColumnName + ', '')'', '''')'
PRINT 'name col name with replace sql = [' + @EnumNameColumnName + ']'
DECLARE @SqlStr VARCHAR(MAX) = 'SELECT ' + @EnumNameColumnName
+ ' + '' = '''
+ ' + LTRIM(RTRIM(STR(' + @EnumValueColumnName + '))) + '','' FROM ' + @TableName + ' ORDER BY ' + @EnumValueColumnName
PRINT 'sql that gets rows for enum body = [' + @SqlStr + ']'
CREATE TABLE #EnumRowsTemp (s VARCHAR(MAX))
INSERT
INTO #EnumRowsTemp
EXEC(@SqlStr)
--SELECT * FROM #EnumRowsTemp
DECLARE @csharpenumbody VARCHAR(MAX)
SELECT @csharpenumbody = COALESCE(@csharpenumbody + ' ', '') + s FROM #EnumRowsTemp
--PRINT @csharpenumbody
DECLARE @csharpenum VARCHAR(MAX) = 'public enum ' + @TableName + ' { ' + @csharpenumbody + ' }'
PRINT @csharpenum
SELECT @csharpenum
DROP TABLE #EnumRowsTemp
Пожалуйста, будьте критичны. Одна забавная вещь, которую я не понял: почему я должен создавать и удалять эту таблицу #EnumRowsTemp, а не просто "SELECT INTO #EnumRowsTemp", чтобы создать временную таблицу на лету? Я не знаю ответа, я пробовал, и это не сработало. Вероятно, это наименьшая из проблем этого кода...
Каким бы грязным это ни было... Я надеюсь, что это сэкономит некоторым из вас, придурки, немного времени.
Вы можете использовать CodeSmith, чтобы сгенерировать что-то вроде этого:
http://www.csharping.com/PermaLink,guid,cef1b637-7d37-4691-8e49-138cbf1d51e9.aspx
Использование динамических перечислений плохо независимо от того, каким образом. Вам придется столкнуться с проблемой "дублирования" данных, чтобы обеспечить ясный и простой код, который легко поддерживать в будущем.
Если вы начнете внедрять автоматически сгенерированные библиотеки, вы наверняка вызовете больше путаницы у будущих разработчиков, которым придется обновлять ваш код, чем просто кодируете enum в соответствующем объекте класса.
Другие приведенные примеры звучат красиво и увлекательно, но подумайте о накладных расходах на обслуживание кода и о том, что вы получите от него. Кроме того, эти значения будут меняться так часто?
Один из способов сохранить Enums и создать динамический список значений одновременно - это использовать Enums, которые у вас есть в настоящее время, с динамически создаваемым словарем.
Поскольку большинство перечислений используются в контексте, в котором они определены для использования, и "динамические перечисления" будут поддерживаться динамическими процессами, вы можете различить 2.
Первым шагом является создание таблицы / коллекции, в которой хранятся идентификаторы и ссылки для динамических записей. В таблице вы получите автоинкремент, значительно превышающий максимальное значение Enum.
Теперь перейдем к части ваших динамических Enums. Я предполагаю, что вы будете использовать Enums для создания набора условий, которые применяют набор правил, некоторые из которых генерируются динамически.
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.
Я не думаю, что есть хороший способ сделать то, что вы хотите. И если вы думаете об этом, я не думаю, что это то, что вы действительно хотите.
Если у вас будет динамическое перечисление, это также означает, что вы должны указать динамическое значение, когда ссылаетесь на него. Может быть, с большим количеством магии вы могли бы достичь своего рода IntelliSense, который позаботится об этом и сгенерирует для вас перечисление в файле DLL. Но рассмотрите объем работы, который потребуется, насколько неэффективным будет доступ к базе данных для получения информации IntelliSense, а также кошмар версии, управляющей сгенерированным файлом DLL.
Если вы действительно не хотите вручную добавлять значения перечислений (вам все равно придется добавить их в базу данных), используйте вместо этого инструмент генерации кода, например шаблоны T4. Щелкните правой кнопкой мыши и запустите, и вы получите статически определенное перечисление в коде, и вы получите все преимущества использования перечислений.
Класс enum builder
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;
}
}
создать объект
string[] types = { "String", "Boolean", "Int32", "Enum", "Point", "Thickness", "long", "float" };
XEnum xe = new XEnum("Enum");
xe.addRange(types);
return xe.getEnum();