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