EFcore与动态模型

xiaoxiao2021-02-27  365

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

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

转载请注明原文地址: https://www.6miu.com/read-1812.html

最新回复(0)