使用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以后新增特性)一起更新。

原文地址:https://www.cnblogs.com/BeanHsiang/p/2482808.html