在開發商城系統的時候,大家會遇到這樣的需求,商城系統里支持多種商品類型,比如衣服,手機,首飾等,每一種產品類型都有自己獨有的參數信息,比如衣服有顏色,首飾有材質等,大家可以上淘寶看一下就明白了。現在的問題是,如果我程式發佈後,要想增加一種新的商品類型怎麼辦,如果不在程式設計時考慮這個問題的話,可能每 ...
在開發商城系統的時候,大家會遇到這樣的需求,商城系統里支持多種商品類型,比如衣服,手機,首飾等,每一種產品類型都有自己獨有的參數信息,比如衣服有顏色,首飾有材質等,大家可以上淘寶看一下就明白了。現在的問題是,如果我程式發佈後,要想增加一種新的商品類型怎麼辦,如果不在程式設計時考慮這個問題的話,可能每增加一個商品類型,就要增加對應商品類型的管理程式,並重新發佈上線,對於維護來說成本會很高。有沒有簡單的方式可以快速增加新類型的支持?下麵介紹的方案是這樣的,首先把模型以配置的方式保存到配置文件中,在程式啟動時解析模型信息編譯成具體的類,然後通過ef實現動態編譯類的資料庫操作,如果新增類型,首先改下配置文件,然後在資料庫中創建對應的資料庫表,重啟應用程式即可。
要實現這樣的功能,需要解決以下幾個問題:
1,如何實現動態模型的配置管理
2,如何根據模型配置在運行時動態生成類型
3,如何讓ef識別動態類型
4,如何結合ef對動態類型信息進行操作,比如查詢,增加等
一、如何實現動態模型的配置管理
這個問題解決的方案是,把模型的信息作為系統的一個配置文件,在系統運行時可以獲取到模型配置信息。
首先定義一個類表示一個動態模型,代碼如下:
public class RuntimeModelMeta { public int ModelId { get; set; } public string ModelName { get; set; }//模型名稱 public string ClassName { get; set; }//類名稱 public string Properties{get;set;}//屬性集合json序列化結果 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等 } }
然後定義個配置類:
public class RuntimeModelMetaConfig { public RuntimeModelMeta[] Metas { get; set; } }
增加配置文件,文件名稱為runtimemodelconfig.json,結構如下:
{ "RuntimeModelMetaConfig": { "Metas": [ { "ModelId": 1, "ModelName": "衣服", "ClassName": "BareDiamond", "ModelProperties": [ { "Name": "尺寸", "PropertyName": "Size", }, { "Name": "顏色", "PropertyName": "Color", } ] } ] } }
下一步再asp.net core mvc的Startup類的構造方法中加入配置文件
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)方法中,獲取到配置信息,代碼如下:
public void ConfigureServices(IServiceCollection services) { 。。。。。。 services.Configure<RuntimeModelMetaConfig>(Configuration.GetSection("RuntimeModelMetaConfig")); 。。。。。。 }
到此就完成了配置信息的管理,在後續代碼中可以通過依賴註入方式獲取到IOptions<RuntimeModelMetaConfig>對象,然後通過IOptions<RuntimeModelMetaConfig>.Value.Metas獲取到所有模型的信息。為了方便模型信息的管理,我這裡定義了一個IRuntimeModelProvider介面,結構如下:
public interface IRuntimeModelProvider { Type GetType(int modelId); Type[] GetTypes(); }
IRuntimeModelProvider.GetType方法可以通過modelId獲取到對應的動態類型Type信息,GetTypes方法返回所有的動態類型信息。這個介面實現請看下麵介紹。
二、如何根據模型配置在運行時動態生成類型
我們有了上面的配置後,需要針對模型動態編譯成對應的類。C#提供了多種運行時動態生成類型的方式,下麵我們介紹通過Emit來生成類,上面的配置信息比較適合模型配置信息的管理,對於生成類的話我們又定義了一個方便另外一個類,代碼如下:
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實現類中,實現代碼如下:
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是所有動態類型的基類,主要是方便屬性的操作,具體代碼如下:
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類,我們來看下這個類的實現,代碼如下:
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中,具體方式如下:
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類補充完整,
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); }
}
在efcore中對象的增加,刪除,更新可以直接使用DbContext就可以完成,比如增加代碼,
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配置信息,就可以動態的把數據呈現到頁面上了。
上面的方案還可以繼續改進,可以把配置信息保存到資料庫中,在程式中增加模型配置管理的功能,實現線上的模型配置,配置改動可以同步操作資料庫表結構,這種方案後續補充上,敬請期待。