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配置信息,就可以動態的把數據呈現到頁面上了。
上面的方案還可以繼續改進,可以把配置信息保存到數據庫中,在程序中增加模型配置管理的功能,實現在線的模型配置,配置改動可以同步操作數據庫表結構,這種方案後續補充上,敬請期待。
EFcore與動態模型