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. IOS之Accessor method

    1 前言 本章主要介绍了Objective-C中的存取方法的相关概念. 2 详述 存储方法是一个可以获得或者设置一个对象的属性值的实例方法.在Cocoa的术语中,一个检索对象属性值的方法提及为gett ...

  2. SPI和RAM IP核

    学习目的: (1) 熟悉SPI接口和它的读写时序: (2) 复习Verilog仿真语句中的$readmemb命令和$display命令: (3) 掌握SPI接口写时序操作的硬件语言描述流程(本例仅以写 ...

  3. 【Unity】第6章 Unity脚本开发基础

    分类:Unity.C#.VS2015 创建日期:2016-04-16 一.简介 游戏吸引人的地方在于它的可交互性.如果游戏没有交互,场景做得再美观和精致,也难以称其为游戏. 在Unity中,游戏交互通 ...

  4. 菜鸟学SSH(十)——Hibernate核心接口

    在使用Hibernate的时候,我们通常都会用的Configuration.SessionFactory.Session.Transaction.Query和Criteria等接口.通过这些接口可以, ...

  5. 菜鸟学Java(五)——JSP内置对象之request

    书接上回,上次跟大家概括的说了说JSP的九种常用内置对象.接下来就该聊聊它们各自的特点了,今天先说说request吧. 下面是request的一些常用方法: isUserInRole(String r ...

  6. Scala解码base64编码的URL提示Last unit does not have enough valid bits

    问题描述 开始使用Base64.getMimeDecoder().decode(base_url).map(_.toChar).mkString去解码,部分数据也提示如题的错误 然后尝试使用Base6 ...

  7. wcout输出中文不显示

    准备使用UNICODE来写个控制台测试程序发现,cout无法输出UNICODE的中文字符.查找c++标准看到,其提供了wcin.wcout.wcerr.wclog用于处理wchar_t字符的输入输出. ...

  8. STM32内部flash存储小数——别样的C语言技巧

    今天在进行STM32内部falsh存储的时候,发现固件库历程的函数原型是这样的: 第一个是地址,在我的STM32中是2K一页的,第二个是要写入的数据. 问题就来了,存储一个小数该怎么办呢?固件库给的是 ...

  9. JAVA-JSP内置对象之request对象的其他方法

    相关资料:<21天学通Java Web开发> request对象的其他方法1.request对象除了可以用来获得请求参数,还可以用来获得HTTP标头及其他信息. 方法           ...

  10. JMeter (2) —— JMeter与WebDriver测试用户登陆以CAS SSO为例(101 Tutorial)

    JMeter (2) -- JMeter与WebDriver测试用户登陆以CAS SSO为例(101 Tutorial) 主要内容 JMeter与WebDriver测试用户登陆以CAS SSO为例 环 ...