使用Emit创建DBContext对象
在EntityFramework Code First的示例中,一般情况下都是要创建一个继承DBContext的类,然后在此类中声明若干DBSet<>的属性,然后才可以使用。最近我就遇到一件为难的事情,项目中的业务对象较多,有一大半是继承了一个自定义的基类ModelBase,如果按照以往的方式就不得不在DBContext里面声明长长的属性,其实就是想有个简便的办法,加上如果后续增加了ModelBase的子类,也不想再去修改DBContext的代码,于是一个念头产生了。
最初我尝试用反射的方式,定义一个方法,传入我想创建的业务对象的类型(Type),结果发现这样是没有办法对一个已存在的类(DBContext)去增加泛型属性DBSet<Type>的。
之后又尝试了创建一个EntityTypeConfiguration<ModelBase>的配置类,然后在其中利用反射,为当前程序集中所有子类都配置映射关系,结果发现基类无可避免地被映射成了一张表,因为EntityTypeConfiguration<ModelBase>的原因,首先就纳入到了DBContext之中了。这并不是我所期望的效果。
于是,我祭出了杀手锏---Emit。基本思路就是动态创建继承DBContext的一个子类对象,根据传入的类来决定是创建其本身或是其子类的DBSet<>属性,这样可控性比较强。
先用一个简单的例子来说明一下。声明一个类
public class Author { public int Id { get; set; } public string Name { get; set; } }
如果按照旧方法,原本应该这样声明
public class Blog : DbContext { public DbSet<Author> Authors { get; set; } }
现在换成如下方式。
public static DbContext CreateInstance() { AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("TestContext"), AssemblyBuilderAccess.RunAndSave); ModuleBuilder mb = ab.DefineDynamicModule("TestContext"); //创建指定名称的类型,注意此名称必须要符合EF的约定,即与连接字符串配置中的Name一致 TypeBuilder tb = mb.DefineType("Blog", TypeAttributes.Public, typeof(DbContext)); PropertyBuilder pbAuthor = tb.DefineProperty("Authors", PropertyAttributes.HasDefault, typeof(DbSet<>).MakeGenericType(typeof(Author)), null); //创建私有字段,用于属性的读写 //不能像c#源代码那样直接声明get/set,因为本质上编译后还是有私有字段的 FieldBuilder fbAuthors = tb.DefineField("_authors", typeof(DbSet<>).MakeGenericType(typeof(Author)), FieldAttributes.Private); //Get方法部分 System.Reflection.MethodAttributes methodAttributes = System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.HideBySig; MethodBuilder mbGet = tb.DefineMethod("get_Authors", methodAttributes); ILGenerator iL = mbGet.GetILGenerator(); Label label = iL.DefineLabel(); iL.Emit(OpCodes.Ldarg_0); iL.Emit(OpCodes.Ldfld, fbAuthors); iL.Emit(OpCodes.Stloc_0); iL.Emit(OpCodes.Br_S, label); iL.MarkLabel(label); iL.Emit(OpCodes.Ldloc_0); iL.Emit(OpCodes.Ret); //Set方法部分 MethodBuilder mbSet = tb.DefineMethod("set_Authors", methodAttributes); mbSet.SetParameters(typeof(System.Data.Entity.DbSet<>).MakeGenericType(typeof(Author))); ParameterBuilder value = mbSet.DefineParameter(1, ParameterAttributes.None, "value"); iL = mbSet.GetILGenerator(); iL.Emit(OpCodes.Ldarg_0); iL.Emit(OpCodes.Ldarg_1); iL.Emit(OpCodes.Stfld, fbAuthors); iL.Emit(OpCodes.Ret); //将定义的方法引用与属性相关联 pbAuthor.SetGetMethod(mbGet); pbAuthor.SetSetMethod(mbSet); DbContext blog = (DbContext)Activator.CreateInstance(tb.CreateType()); return blog; }
最后来一段测试的代码
using (DbContext blog = CreateInstance())
{
Author author = new Author { Name = "123" }; blog.Set<Author>().Add(author); blog.SaveChanges(); }
总结一下,其实Emit创建DbContext的重心就在于对每一个需要操作的业务对象都创建一对Get/Set方法,上述代码可以进一步提炼,单独写成一个方法,传入不同的Type替换掉Author所占的位置即可。此方法很另类,性能上也并非很理想(在对象数量比较庞大的情况下反而效率较高),因此在使用EF的过程中就初始化的时候创建一次即可,在有对象的变更或增减时,可以随着Migration(EF4.3以后新增特性)一起更新。