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