EFcore与动态模型

  在开发商城系统的时候,大家会遇到这样的需求,商城系统里支持多种商品类型,比如衣服,手机,首饰等,每一种产品类型都有自己独有的参数信息,比如衣服有颜色,首饰有材质等,大家可以上淘宝看一下就明白了。现在的问题是,如果我程序发布后,要想增加一种新的商品类型怎么办,如果不在程序设计时考虑这个问题的话,可能每增加一个商品类型,就要增加对应商品类型的管理程序,并重新发布上线,对于维护来说成本会很高。有没有简单的方式可以快速增加新类型的支持?下面介绍的方案是这样的,首先把模型以配置的方式保存到配置文件中,在程序启动时解析模型信息编译成具体的类,然后通过ef实现动态编译类的数据库操作,如果新增类型,首先改下配置文件,然后在数据库中创建对应的数据库表,重启应用程序即可。

  要实现这样的功能,需要解决以下几个问题:

  1,如何实现动态模型的配置管理

  2,如何根据模型配置在运行时动态生成类型

  3,如何让ef识别动态类型

  4,如何结合ef对动态类型信息进行操作,比如查询,增加等

  一、如何实现动态模型的配置管理

  这个问题解决的方案是,把模型的信息作为系统的一个配置文件,在系统运行时可以获取到模型配置信息。

  首先定义一个类表示一个动态模型,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  class  RuntimeModelMeta
    {
        public  int  ModelId {  get set ; }
        public  string  ModelName {  get set ; } //模型名称
        public  string  ClassName {  get set ; } //类名称
        public  ModelPropertyMeta[] ModelProperties {  get set ; }
        
        public  class  ModelPropertyMeta
        {
            public  string  Name {  get set ; } //对应的中文名称
            public  string  PropertyName {  get set ; }  //类属性名称 
       public  int  Length {  get set ; } //数据长度,主要用于string类型
 
        public  bool  IsRequired {  get set ; } //是否必须输入,用于数据验证
        public  string  ValueType {  get set ; } //数据类型,可以是字符串,日期,bool等
        }
    }

  

  然后定义个配置类:

1
2
3
4
public  class  RuntimeModelMetaConfig
   {
       public  RuntimeModelMeta[] Metas {  get set ; }
   }

  增加配置文件,文件名称为runtimemodelconfig.json,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
   "RuntimeModelMetaConfig" : {
     "Metas" : [
       {
         "ModelId" : 1,
         "ModelName" "衣服" ,
         "ClassName" "BareDiamond" ,
        
         "ModelProperties" : [
           {
             "Name" "尺寸" ,
             "PropertyName" "Size" ,
           },
           {
             "Name" "颜色" ,
             "PropertyName" "Color" ,
           }
         ]
       }
     ]
   }
}

  下一步再asp.net core mvc的Startup类的构造方法中加入配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  Startup(IHostingEnvironment env)
     {
         var  builder =  new  ConfigurationBuilder()
             .SetBasePath(env.ContentRootPath)
             .AddJsonFile( "appsettings.json" , optional:  true , reloadOnChange:  true )
             .AddJsonFile( "runtimemodelconfig.json" , optional: true ,reloadOnChange: true )
             .AddEnvironmentVariables();
 
         if  (env.IsDevelopment())
         {
             builder.AddApplicationInsightsSettings(developerMode:  true );
         }
         Configuration = builder.Build();
     }

  然后再public void ConfigureServices(IServiceCollection services)方法中,获取到配置信息,代码如下:

1
2
3
4
5
6
public  void  ConfigureServices(IServiceCollection services)
    {
        。。。。。。
        services.Configure<RuntimeModelMetaConfig>(Configuration.GetSection( "RuntimeModelMetaConfig" ));
       。。。。。。
    }

  到此就完成了配置信息的管理,在后续代码中可以通过依赖注入方式获取到IOptions<RuntimeModelMetaConfig>对象,然后通过IOptions<RuntimeModelMetaConfig>.Value.Metas获取到所有模型的信息。为了方便模型信息的管理,我这里定义了一个IRuntimeModelProvider接口,结构如下:

1
2
3
4
5
public  interface  IRuntimeModelProvider
     {
         Type GetType( int  modelId);
     Type[] GetTypes();
     }

   IRuntimeModelProvider.GetType方法可以通过modelId获取到对应的动态类型Type信息,GetTypes方法返回所有的动态类型信息。这个接口实现请看下面介绍。

  二、如何根据模型配置在运行时动态生成类型

  我们有了上面的配置后,需要针对模型动态编译成对应的类。C#提供了多种运行时动态生成类型的方式,下面我们介绍通过Emit来生成类,上面的配置信息比较适合模型配置信息的管理,对于生成类的话我们又定义了一个方便另外一个类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public  class  TypeMeta
   {
       public  TypeMeta()
       {
           PropertyMetas =  new  List<TypePropertyMeta>();
           AttributeMetas =  new  List<AttributeMeta>();
       }
       public  Type BaseType {  get set ; }
       public  string  TypeName {  get set ; }
       public  List<TypePropertyMeta> PropertyMetas {  get set ; }
       public  List<AttributeMeta> AttributeMetas {  get set ; }
 
       public  class  TypePropertyMeta
       {
           public  TypePropertyMeta()
           {
               AttributeMetas =  new  List<AttributeMeta>();
           }
           public  Type PropertyType {  get set ; }
           public  string  PropertyName {  get set ; }
           public  List<AttributeMeta> AttributeMetas {  get set ; }
       }
 
       public  class  AttributeMeta
       {
           public  Type AttributeType {  get set ; }
           public  Type[] ConstructorArgTypes {  get set ; }
           public  object [] ConstructorArgValues {  get set ; }
           public  string [] Properties {  get set ; }
           public  object [] PropertyValues {  get set ; }
       }
   }

  上面的类信息更接近一个类的定义,我们可以把一个RuntimeModelMeta转换成一个TypeMeta,我们把这个转换过程放到IRuntimeModelProvider实现类中,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public  class  DefaultRuntimeModelProvider : IRuntimeModelProvider
     {
         private  Dictionary< int , Type> _resultMap;
         private  readonly  IOptions<RuntimeModelMetaConfig> _config;
         private  object  _lock =  new  object ();
         public  DefaultRuntimeModelProvider(IOptions<RuntimeModelMetaConfig> config)
         {
             //通过依赖注入方式获取到模型配置信息
             _config = config;
         }
       //动态编译结果的缓存,这样在获取动态类型时不用每次都编译一次
         public  Dictionary< int , Type> Map
         {
             get
             {
                 if  (_resultMap ==  null )
                 {
                     lock  (_lock)
                     {
                         _resultMap =  new  Dictionary< int , Type>();
                 
                         foreach  ( var  item  in  _config.Value.Metas)
                         {
                             //根据RuntimeModelMeta编译成类,具体实现看后面内容
                             var  result = RuntimeTypeBuilder.Build(GetTypeMetaFromModelMeta(item));
                            //编译结果放到缓存中,方便下次使用
                             _resultMap.Add(item.ModelId, result);
                         }
                     }
                 }
                 return  _resultMap;
             }
         }
 
         public  Type GetType( int  modelId)
         {
             Dictionary< int , Type> map = Map;
             Type result =  null ;
             if  (!map.TryGetValue(modelId,  out  result))
             {
                 throw  new  NotSupportedException( "dynamic model not supported:"  + modelId);
             }
             return  result;
         }
 
         public  Type[] GetTypes()
         {
             int [] modelIds = _config.Value.Metas.Select(m => m.ModelId).ToArray();
             return  Map.Where(m => modelIds.Contains(m.Key)).Select(m => m.Value).ToArray();           
         }
         //这个方法就是把一个RuntimeModelMeta转换成更接近类结构的TypeMeta对象
         private  TypeMeta GetTypeMetaFromModelMeta(RuntimeModelMeta meta)
         {
             TypeMeta typeMeta =  new  TypeMeta();
             //我们让所有的动态类型都继承自DynamicEntity类,这个类主要是为了方便属性数据的读取,具体代码看后面
             typeMeta.BaseType =  typeof (DynamicEntity);
             typeMeta.TypeName = meta.ClassName;
            
             foreach  ( var  item  in  meta.ModelProperties)
             {
                 TypeMeta.TypePropertyMeta pmeta =  new  TypeMeta.TypePropertyMeta();
                 pmeta.PropertyName = item.PropertyName;
                 //如果必须输入数据,我们在属性上增加RequireAttribute特性,这样方便我们进行数据验证
                 if  (item.IsRequired)
                 {
                     TypeMeta.AttributeMeta am =  new  TypeMeta.AttributeMeta();
                     am.AttributeType =  typeof (RequiredAttribute);
                     am.Properties =  new  string [] {  "ErrorMessage"  };
                     am.PropertyValues =  new  object [] {  "请输入"  + item.Name };
                     pmeta.AttributeMetas.Add(am);
                 }
                 
                 if  (item.ValueType ==  "string" )
                 {
                     pmeta.PropertyType =  typeof ( string );
                     TypeMeta.AttributeMeta am =  new  TypeMeta.AttributeMeta();
                     //增加长度验证特性
                      am.AttributeType =  typeof (StringLengthAttribute);
                      am.ConstructorArgTypes =  new  Type[] {  typeof ( int ) };
                      am.ConstructorArgValues =  new  object [] { item.Length };
                      am.Properties =  new  string [] {  "ErrorMessage"  };
                      am.PropertyValues =  new  object [] { item.Name +  "长度不能超过"  + item.Length.ToString() +  "个字符"  };
                        
                      pmeta.AttributeMetas.Add(am);
                 }
                 else  if (item.ValueType== "int" )
                 {
                     if  (!item.IsRequired)
                     {
                         pmeta.PropertyType =  typeof ( int ?);
                     }
                     else
                     {
                         pmeta.PropertyType =  typeof ( int );
                     }
                 }
                 else  if  (item.ValueType== "datetime" )
                 {
                     if  (!item.IsRequired)
                     {
                         pmeta.PropertyType =  typeof (DateTime?);
                     }
                     else
                     {
                         pmeta.PropertyType =  typeof (DateTime);
                     }
                 }
                 else  if  (item.ValueType ==  "bool" )
                 {
                     if  (!item.IsRequired)
                     {
                         pmeta.PropertyType =  typeof ( bool ?);
                     }
                     else
                     {
                         pmeta.PropertyType =  typeof ( bool );
                     }
                 }
                 typeMeta.PropertyMetas.Add(pmeta);
             }
             return  typeMeta;
         }
     }

  

  DynamicEntity是所有动态类型的基类,主要是方便属性的操作,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public  class  DynamicEntity: IExtensible
     {
         private  Dictionary< object object > _attrs;
         public  DynamicEntity()
         {
             _attrs =  new  Dictionary< object object >();
         }
         public  DynamicEntity(Dictionary< object , object > dic)
         {
             _attrs = dic;
         }
         public  static  DynamicEntity Parse( object  obj)
         {
             DynamicEntity model =  new  DynamicEntity();
             foreach  (PropertyInfo info  in  obj.GetType().GetProperties())
             {
                 model._attrs.Add(info.Name, info.GetValue(obj,  null ));
             }
             return  model;
         }
         public  T GetValue<T>( string  field)
         {
             object  obj2 =  null ;
             if (!_attrs.TryGetValue(field,  out  obj2))
             {
                 _attrs.Add(field,  default (T));
             }
             if  (obj2 ==  null )
             {
                 return  default (T);
             }
             return  (T)obj2;
         }
 
         public  void  SetValue<T>( string  field, T value)
         {
             if  (_attrs.ContainsKey(field))
             {
                 _attrs[field] = value;
             }
             else
             {
                 _attrs.Add(field, value);
             }
         }
 
         [JsonIgnore]
         public  Dictionary< object object > Attrs
         {
             get
             {
                 return  _attrs;
             }
         }
     //提供索引方式操作属性值
         public  object  this [ string  key]
         {
             get
             {
                 object  obj2 =  null ;
                 if  (_attrs.TryGetValue(key,  out  obj2))
                 {
                     return  obj2;
                 }
                 return  null ;
             }
             set
             {
                 if  (_attrs.Any(m =>  string .Compare(m.Key.ToString(), key,  true ) != -1))
                 {
                     _attrs[key] = value;
                 }
                 else
                 {
                     _attrs.Add(key, value);
                 }
             }
         }
         [JsonIgnore]
         public  string [] Keys
         {
             get
             {
                 return  _attrs.Keys.Select(m=>m.ToString()).ToArray();
             }
         }
 
         public  int  Id
         {
             get
             {
                 return  GetValue< int >( "Id" );
             }
             set
             {
                 SetValue( "Id" , value);
             }
         }
         [Timestamp]
         [JsonIgnore]
         public  byte [] Version {  get set ; }
     }

  

  另外在上面编译类的时候用到了RuntimeTypeBuilder类,我们来看下这个类的实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public  static  class  RuntimeTypeBuilder
     {
         private  static  ModuleBuilder moduleBuilder;
         static  RuntimeTypeBuilder()
         {
             AssemblyName an =  new  AssemblyName( "__RuntimeType" );
             moduleBuilder = AssemblyBuilder.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run).DefineDynamicModule( "__RuntimeType" );
         }
         public  static  Type Build(TypeMeta meta)
         {
             TypeBuilder builder = moduleBuilder.DefineType(meta.TypeName, TypeAttributes.Public);
             CustomAttributeBuilder tableAttributeBuilder =  new  CustomAttributeBuilder( typeof (TableAttribute).GetConstructor( new  Type[1] {  typeof ( string )}),  new  object [] {  "RuntimeModel_"  + meta.TypeName });
             builder.SetParent(meta.BaseType);
             builder.SetCustomAttribute(tableAttributeBuilder);
             
             foreach  ( var  item  in  meta.PropertyMetas)
             {
                 AddProperty(item, builder, meta.BaseType);
             }
             return  builder.CreateTypeInfo().UnderlyingSystemType;
         }
        
         private  static  void  AddProperty(TypeMeta.TypePropertyMeta property, TypeBuilder builder,Type baseType)
         {
             PropertyBuilder propertyBuilder = builder.DefineProperty(property.PropertyName, PropertyAttributes.None, property.PropertyType,  null );
 
             foreach  ( var  item  in  property.AttributeMetas)
             {
                 if  (item.ConstructorArgTypes== null )
                 {
                     item.ConstructorArgTypes =  new  Type[0];
                     item.ConstructorArgValues =  new  object [0];
                 }
                 ConstructorInfo cInfo = item.AttributeType.GetConstructor(item.ConstructorArgTypes);
                 PropertyInfo[] pInfos = item.Properties.Select(m => item.AttributeType.GetProperty(m)).ToArray();
                 CustomAttributeBuilder aBuilder =  new  CustomAttributeBuilder(cInfo, item.ConstructorArgValues, pInfos, item.PropertyValues);
                 propertyBuilder.SetCustomAttribute(aBuilder);
             }
 
             MethodAttributes attributes = MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Public;
             MethodBuilder getMethodBuilder = builder.DefineMethod( "get_"  + property.PropertyName, attributes, property.PropertyType, Type.EmptyTypes);
             ILGenerator iLGenerator = getMethodBuilder.GetILGenerator();
             MethodInfo getMethod = baseType.GetMethod( "GetValue" ).MakeGenericMethod( new  Type[] { property.PropertyType });
             iLGenerator.DeclareLocal(property.PropertyType);
             iLGenerator.Emit(OpCodes.Nop);
             iLGenerator.Emit(OpCodes.Ldarg_0);
             iLGenerator.Emit(OpCodes.Ldstr, property.PropertyName);
             iLGenerator.EmitCall(OpCodes.Call, getMethod,  null );
             iLGenerator.Emit(OpCodes.Stloc_0);
             iLGenerator.Emit(OpCodes.Ldloc_0);
             iLGenerator.Emit(OpCodes.Ret);
             MethodInfo setMethod = baseType.GetMethod( "SetValue" ).MakeGenericMethod( new  Type[] { property.PropertyType });
             MethodBuilder setMethodBuilder = builder.DefineMethod( "set_"  + property.PropertyName, attributes,  null new  Type[] { property.PropertyType });
             ILGenerator generator2 = setMethodBuilder.GetILGenerator();
             generator2.Emit(OpCodes.Nop);
             generator2.Emit(OpCodes.Ldarg_0);
             generator2.Emit(OpCodes.Ldstr, property.PropertyName);
             generator2.Emit(OpCodes.Ldarg_1);
             generator2.EmitCall(OpCodes.Call, setMethod,  null );
             generator2.Emit(OpCodes.Nop);
             generator2.Emit(OpCodes.Ret);
             propertyBuilder.SetGetMethod(getMethodBuilder);
             propertyBuilder.SetSetMethod(setMethodBuilder);
 
         }
 
 
     }

  主要部分是ILGenerator的使用,具体使用方式大家可以查阅相关资料,这里不再详细介绍。

  三、如何让ef识别动态类型

  在ef中操作对象需要借助DbContext,如果静态的类型,那我们就可以在定义DbContext的时候,增加DbSet<TEntity>类型的属性即可,但是我们现在的类型是在运行时生成的,那怎么样才能让DbContext能够认识这个类型,答案是OnModelCreating方法,在这个方法中,我们把动态模型加入到DbContext中,具体方式如下:

1
2
3
4
5
6
7
8
9
10
11
protected  override  void  OnModelCreating(ModelBuilder modelBuilder)
       {
      //_modelProvider就是我们上面定义的IRuntimeModelProvider,通过依赖注入方式获取到实例
           Type[] runtimeModels = _modelProvider.GetTypes( "product" );
           foreach  ( var  item  in  runtimeModels)
           {
               modelBuilder.Model.AddEntityType(item);
           }
 
           base .OnModelCreating(modelBuilder);
       }

  

  这样在我们DbContext就能够识别动态类型了。注册到DbContext很简单,关键是如何进行信息的操作。

  四、如何结合ef对动态信息进行操作

  我们先把上面的DbContext类补充完整,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public  class  ShopDbContext : DbContext
   {
       private  readonly  IRuntimeModelProvider _modelProvider;
       public  ShopDbContext(DbContextOptions<ShopDbContext> options, IRuntimeModelProvider modelProvider)
           base (options)
       {
           _modelProvider = modelProvider;
       }
 
       protected  override  void  OnModelCreating(ModelBuilder modelBuilder)
       {
           Type[] runtimeModels = _modelProvider.GetTypes( "product" );
           foreach  ( var  item  in  runtimeModels)
           {
               modelBuilder.Model.AddEntityType(item);
           }
 
           base .OnModelCreating(modelBuilder);
       }<br>}

  

  在efcore中对象的增加,删除,更新可以直接使用DbContext就可以完成,比如增加代码,

1
2
ShopDbContext.Add(entity);
ShopDbContext.SaveChanges();

  更新操作比较简单,比较难解决的是查询,包括查询条件设置等等。国外有大牛写了一个LinqDynamic,我又对它进行了修改,并增加了一些异步方法,代码我就不粘贴到文章里了,大家可以直接下载源码:下载linqdynamic

  LinqDynamic中是对IQueryable的扩展,提供了动态linq的查询支持,具体使用方法大家可以百度。efcore中DbSet泛型定义如下:

  

  public abstract partial class DbSet<TEntity>: IQueryable<TEntity>, IAsyncEnumerableAccessor<TEntity>, IInfrastructure<IServiceProvider>

  不难发现,它就是一个IQueryable<TEntity>,而IQueryable<TEntity>又是一个IQueryable,正好是LinqDynamic需要的类型,所以我们现在需要解决的是根据动态模型信息,获取到一个IQueryable,我采用反射方式获取:

   ShopDbContext.GetType().GetTypeInfo().GetMethod("Set").MakeGenericMethod(type).Invoke(context, null) as IQueryable;

  有了IQueryable,就可以使用LinqDynamic增加的扩展方式,实现动态查询了。查询到的结果是一个动态类型,但是我们前面提到,我们所有的动态类型都是一个DynamicEntity类型,所以我们要想访问某个属性的值的时候,我们可以直接采用索引的方式读取,比如obj["属性"],然后结合RuntimeModelMeta配置信息,就可以动态的把数据呈现到页面上了。

  上面的方案还可以继续改进,可以把配置信息保存到数据库中,在程序中增加模型配置管理的功能,实现在线的模型配置,配置改动可以同步操作数据库表结构,这种方案后续补充上,敬请期待。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值