With EntityFramework’s support for enums, there is no longer any need to include lookup tables in the model. But I do want to have them in the database for integrity, even with code first.

I’ve been thinking for some time about to handle enums with code first. The idea behind code first is to be able to write the code as close as possible to how object oriented code is normally written. For enums that means that the enum definition itself is the constraint that is used to ensure that only valid values are used in the code.

For databases it would be possible to use a column constraint, but the normal way is to use a lookup table where the valid values are present. Any column in the database mapped against the enum type is then a foreign key to the lookup table to ensure integrity of the data.

What I would prefer is a solution where the lookup table is present in the database, but not mapped against any entity in the code.

Adding the Lookup Table

Adding the lookup table is simple when using Entity Framework Migrations. I’ve reused my old example on creating a database and updated Entity Framework to 6.0.1 that supports enums. When scaffolding a migration, theCarBodyStyle field (which is an enum) is now recognized by entity framework:

public override void Up()
{
AddColumn("dbo.Cars", "BodyStyle", c => c.Int(nullable: false));
}

The lookup table and the foreign key can be added by extending the migration code:

public override void Up()
{
CreateTable("dbo.CarBodyStyles",
c => new
{
Id = c.Int(nullable: false),
Description = c.String(maxLength: 50)
}).PrimaryKey(t => t.Id);
 
Sql("INSERT CarBodyStyles VALUES (0, 'Not Defined') ");
 
AddColumn("dbo.Cars", "BodyStyle", c => c.Int(nullable: false));
 
AddForeignKey("dbo.Cars", "BodyStyle", "dbo.CarBodyStyles");
}

The insert of the first value in CarBodyStyles is there to make sure that there is a matching value when the foreign key constraint is added to the Cars table. Without it, the foreign key can’t be added.

Adding Lookup Values

The migration takes care of adding the lookup table to the database, but that’s only the first half of the solution. The second half is to make sure that all the enum values are reflected in the lookup table automatically. The right place to do that is not in a migration, but in the Configuration.Seed() method that is called every time the migrations are applied. It is called even if there are no pending migrations.

protected override void Seed(TestLib.Entities.CarsContext context)
{
context.Seed<TestLib.Entities.CarBodyStyle>();
}

That looks simple – doesn’t it? All of the work is deferred to the Seed<TEnum>() extension method. The lookup table isn’t mapped to the entity model, so there is no way to use all the nice features of entity framework for this operation. Instead I’m building up a plain Sql string, following the pattern from the Idempotent DB update Scripts post. I know that I’m not escaping data nor using parameterized Sql which leaves the risk för Sql injection. On the other hand I’m only using data from description attributes which is controlled by the developer so if you want to use it and Sql inject yourself – go ahead!

public static class EnumSeeder
{
/// <summary>
/// Populate a table with values based on defined enum values.
/// </summary>
/// <typeparam name="TEnum">Type of the enum</typeparam>
/// <param name="context">A DbContext to use to run queries against
/// the database.</param>
/// <param name="idField">Id field, that should be populated with
/// the numeric value of the enum.</param>
/// <param name="descriptionField">Description field, that should be
/// populated with the contents of the Description attribute (if
/// there is any defined).</param>
/// <param name="tableName">Name of the table. Assumed to be the same
/// as the enum name plus an "s" for pluralization if nothing
/// else is defined</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1062:Validate arguments of public methods", MessageId = "0"),
System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1004:GenericMethodsShouldProvideTypeParameter")]
public static void Seed<TEnum>(this DbContext context, string idField = "Id",
string descriptionField = "description", string tableName = null)
{
if (tableName == null)
{
tableName = typeof(TEnum).Name + "s";
}
 
var commandBuilder = new StringBuilder();
 
commandBuilder.AppendFormat("CREATE TABLE #EnumValues (\n" +
"Id {0} NOT NULL PRIMARY KEY,\n" +
"Description NVARCHAR(50))\n\n", GetIdType<TEnum>());
 
AddValues<TEnum>(commandBuilder);
 
string descriptionUpdate = descriptionField == null ? string.Empty :
string.Format(CultureInfo.InvariantCulture,
"WHEN MATCHED THEN UPDATE\n" +
"SET dst.{0} = src.Description\n", descriptionField);
 
string descriptionInsert = descriptionField == null ? string.Empty :
", src.Description";
 
string descriptionInFieldList = descriptionField == null ? string.Empty :
", " + descriptionField;
 
commandBuilder.AppendFormat(CultureInfo.InvariantCulture,
"MERGE {0} dst\n" +
"USING #EnumValues src\n" +
"ON (src.Id = dst.{1})\n" +
"{2}" +
"WHEN NOT MATCHED THEN\n" +
"INSERT ({1}{3}) VALUES (src.Id{4})\n" +
"WHEN NOT MATCHED BY SOURCE THEN DELETE;\n\n",
tableName, idField, descriptionUpdate, descriptionInFieldList, descriptionInsert);
 
commandBuilder.AppendFormat(CultureInfo.InvariantCulture, "DROP TABLE #EnumValues\n");
 
context.Database.ExecuteSqlCommand(TransactionalBehavior.DoNotEnsureTransaction,
commandBuilder.ToString());
}
 
private static void AddValues<TEnum>(StringBuilder commandBuilder)
{
var values = Enum.GetValues(typeof(TEnum));
 
if (values.Length > 0)
{
commandBuilder.AppendFormat(CultureInfo.InvariantCulture,
"INSERT #EnumValues VALUES\n");
 
var descriptions = GetDescriptions<TEnum>();
 
bool firstValue = true;
foreach (var v in values)
{
if (firstValue)
{
firstValue = false;
}
else
{
commandBuilder.AppendFormat(CultureInfo.InvariantCulture, ",\n");
}
string valueString = v.ToString();
 
commandBuilder.AppendFormat(CultureInfo.InvariantCulture, "({0}, '{1}')",
(int)v, descriptions[valueString]);
}
 
commandBuilder.AppendFormat(CultureInfo.InvariantCulture, "\n\n");
}
}
 
private static IDictionary<string, string> GetDescriptions<TEnum>()
{
return typeof(TEnum).GetMembers(BindingFlags.Static | BindingFlags.Public)
.Select(m => new
{
Name = m.Name,
Description = m.GetCustomAttributes(typeof(DescriptionAttribute), true)
.Cast<DescriptionAttribute>().SingleOrDefault()
})
.ToDictionary(a => a.Name,
a => a.Description == null ? null : a.Description.Description);
}
 
private static string GetIdType<TEnum>()
{
var underlyingType = Enum.GetUnderlyingType(typeof(TEnum));
 
if(underlyingType == typeof(int))
{
return "INT";
}
 
if(underlyingType == typeof(short))
{
return "SMALLINT";
}
 
if(underlyingType == typeof(byte))
{
return "TINYINT";
}
 
throw new NotImplementedException();
}
}

Enums and Lookup Tables with EF Code First的更多相关文章

  1. Using Lookup Tables to Accelerate Color Transformations

    转自:http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter24.html In feature-film visual-effects ...

  2. Inheritance with EF Code First: Part 3 – Table per Concrete Type (TPC)

    Inheritance with EF Code First: Part 3 – Table per Concrete Type (TPC) This is the third (and last) ...

  3. Inheritance with EF Code First: Part 2 – Table per Type (TPT)

    In the previous blog post you saw that there are three different approaches to representing an inher ...

  4. Inheritance with EF Code First: Part 1 – Table per Hierarchy (TPH)

    以下三篇文章是Entity Framework Code-First系列中第七回:Entity Framework Code-First(7):Inheritance Strategy 提到的三篇.这 ...

  5. EF Code First Migrations数据库迁移

    1.EF Code First创建数据库 新建控制台应用程序Portal,通过程序包管理器控制台添加EntityFramework. 在程序包管理器控制台中执行以下语句,安装EntityFramewo ...

  6. EF Code First学习系列

    EF Model First在实际工作中基本用不到,前段时间学了一下,大概的了解一下.现在开始学习Code First这种方式.这也是在实际工作中用到最多的方式. 下面先给出一些目录: 1.什么是Co ...

  7. EF和MVC系列文章导航:EF Code First、DbContext、MVC

    对于之前一直使用webForm服务器控件.手写ado.net操作数据库的同学,突然来了EF和MVC,好多新概念泉涌而出,的确犹如当头一棒不知所措.本系列文章可以帮助新手入门并熟练使用EF和MVC,有了 ...

  8. EF Code First 初体验

    Code First 顾名思义就是先代码,再由代码生成数据库的开发方式. 废话不多说,直接来一发看看:在VS2010里新建一个空白解决方案,再依次添加两个类库项目:Model.DataAccess和一 ...

  9. 【极力分享】[C#/.NET]Entity Framework(EF) Code First 多对多关系的实体增,删,改,查操作全程详细示例【转载自https://segmentfault.com/a/1190000004152660】

      [C#/.NET]Entity Framework(EF) Code First 多对多关系的实体增,删,改,查操作全程详细示例 本文我们来学习一下在Entity Framework中使用Cont ...

随机推荐

  1. android辅助开发工具包介绍

    辅助开发工具包(ADK)是为硬件制造商和业余爱好者准备的参考实现.硬件制造商和业余爱好者可以使用此工具包作为开发Android辅助设备的起点.每一个ADK发行版都将提供源代码和硬件规格,以使整个辅助设 ...

  2. java.lang.SecurityException:Servlet of class org.apache.catalina.servlets.InvokerServlet is privileged

    转自:http://www.cnblogs.com/orientsun/archive/2012/11/04/2753270.html Error: java.lang.SecurityExcepti ...

  3. VS2015终极卸载方法

    今天打开VS2015发现出问题了,总是停止响应,去控制面板里卸载结果像下面这样,卸载出错!于是我有开始折腾了,重新安装一遍然后,还是有问题,在卸载还是出错于是我决定通过安装介质卸载,结果,悲剧的是,启 ...

  4. [Windows Azure] Managing SQL Database using SQL Server Management Studio

    Managing Windows Azure SQL Database using SQL Server Management Studio You can use Windows Azure SQL ...

  5. 关于RPG游戏结构撰写的相关探索上篇

    本章节的目标是创造一个游戏理念.这个理念是: *简短的项目概括 *范围描述 *目标用户 *与其他游戏的区别 不要试图编写一款缺乏明确理念的RPG.因为这样可能只会产生与其他游戏雷同的项目. <i ...

  6. HTTP 代理服务器技术选型之旅

    HTTP 代理服务器技术选型之旅 背景 长期以来,贴吧开发人员多,业务耦合大,需求变化频繁,因此容易产生 bug.而我所负责的广告相关业务,和 UI 密切相关,一旦因为某种原因(甚至是被别人改了代码) ...

  7. JMeter测试工具中的参数化使用[函数助手]

    下面是我使用JMeter测试工具对某项目中的一个http接口进行测试时的配置参数,使用到了JMeter的随机函数: 在发送POST请求时,需要发送json格式的数据,其中使用到JMeter的" ...

  8. 12.22日wagas学习笔记

    第一部分:vue-cli3 vue.config.js配置: baseUrl:process.env.NODE_ENV === 'production'?'/production-sub-path': ...

  9. JS中的history对象

    window.history指向History对象,它表示当前窗口的浏览历史. History对象保存了当前窗口访问过的所有页面网址. history.back()//后退到前一个网址 等同于 his ...

  10. 腾讯云提示invalid pos, pos is bigger than filesize! pos: 0, file_size: 0错误

    腾讯云提示invalid pos, pos is bigger than filesize! pos: 0, file_size: 0错误 起因 使用hdfs dfs -text xxx命令提示下面错 ...