最近受博客園某篇文章的博主啟發,研究了一下用c#的動態代理(Dynamic Proxy)模式監控實體屬性的變更。
背景知識:用ORM實體框架怎么樣去更新實體,就拿Entity Framework舉個例子,EF提供了2種方式去更改
1. 先去數據庫獲取實體,然后在實體上進行修改,修改后調用SaveChanges,此時EF會根據你修改的屬性動態生成部分字段的更新
代碼如下:
using (MatureCMSDbContext dbContext = new MatureCMSDbContext()) {
Category category = dbContext.Set<Category>().FirstOrDefault(p => p.Id == 1); category.Name = "Test name"; dbContext.SaveChanges(); }
生成的SQL為:
exec sp_executesql N'update [dbo].[Categories] set [Name] = @0 where ([Id] = @1) ',N'@0 nvarchar(20),@1 bigint',@0=N'Test name',@1=1
2. 不想獲取一次數據,可以把修改后的實體Attach到DbContext.Set<T>中,顯示修改Entry的狀態為Modified,修改后調用SaveChanges,此時EF生成的sql語句是更新所有字段的
代碼如下:
using (MatureCMSDbContext dbContext = new MatureCMSDbContext()) { Category category = new Category(); category.Id = 1; category.Name = "Test name"; dbContext.Set<Category>().Attach(category); dbContext.Entry<Category>(category).State = System.Data.EntityState.Modified; dbContext.SaveChanges(); }
生成的SQL為:
exec sp_executesql N'update [dbo].[Categories] set [Name] = @0, [Description] = null, [ParentCategoryId] = null where ([Id] = @1) ',N'@0 nvarchar(20),@1 bigint',@0=N'Test name',@1=1
如果想要訪問數據庫一次且又能根據屬性的變化動態生成部分字段的更新呢?這就是我寫這篇文章的目地
下面是我寫的一個代理類的生成器DynamicProxyGenerator,具體代碼自己看吧,應該都能看懂
public class DynamicProxyGenerator { private const string DynamicAssemblyName = "DynamicAssembly";//動態程序集名稱 private const string DynamicModuleName = "DynamicAssemblyModule"; private const string DynamicModuleDllName = "DynamicAssembly.dll";//動態模塊名稱 private const string ProxyClassNameFormater = "{0}Proxy"; private const string ModifiedPropertyNamesFieldName = "ModifiedPropertyNames"; private const MethodAttributes GetSetMethodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.CheckAccessOnOverride | MethodAttributes.Virtual | MethodAttributes.HideBySig; /// <summary> /// 創建動態程序集,返回AssemblyBuilder /// </summary> /// <param name="isSavaDll"></param> /// <returns></returns> private static AssemblyBuilder DefineDynamicAssembly(bool isSavaDll = false) { //動態創建程序集 AssemblyName DemoName = new AssemblyName(DynamicAssemblyName); AssemblyBuilderAccess assemblyBuilderAccess = isSavaDll ? AssemblyBuilderAccess.RunAndSave : AssemblyBuilderAccess.Run; AssemblyBuilder dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(DemoName, assemblyBuilderAccess); return dynamicAssembly; } /// <summary> /// 創建動態模塊,返回ModuleBuilder /// </summary> /// <param name="isSavaDll"></param> /// <returns>ModuleBuilder</returns> private static ModuleBuilder DefineDynamicModule(AssemblyBuilder dynamicAssembly, bool isSavaDll = false) { ModuleBuilder moduleBuilder = null; //動態創建模塊 if(isSavaDll) moduleBuilder = dynamicAssembly.DefineDynamicModule(DynamicModuleName, DynamicModuleDllName); else moduleBuilder = dynamicAssembly.DefineDynamicModule(DynamicModuleName); return moduleBuilder; } /// <summary> /// 創建動態代理類,重寫屬性Get Set 方法,並監控屬性的Set方法,把變更的屬性名加入到list集合中,需要監控的屬性必須是virtual /// 如果你想保存修改的屬性名和屬性值,修改Set方法的IL實現 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="isSavaDynamicModule"></param> /// <returns></returns> public static T CreateDynamicProxy<T>(bool isSavaDynamicModule = false) { Type modifiedPropertyNamesType = typeof(HashSet<string>); Type typeNeedProxy = typeof(T); AssemblyBuilder assemblyBuilder = DefineDynamicAssembly(isSavaDynamicModule); //動態創建模塊 ModuleBuilder moduleBuilder = DefineDynamicModule(assemblyBuilder,isSavaDynamicModule); string proxyClassName = string.Format(ProxyClassNameFormater, typeNeedProxy.Name); //動態創建類代理 TypeBuilder typeBuilderProxy = moduleBuilder.DefineType(proxyClassName, TypeAttributes.Public, typeNeedProxy); //定義一個變量存放屬性變更名 FieldBuilder fbModifiedPropertyNames = typeBuilderProxy.DefineField(ModifiedPropertyNamesFieldName, modifiedPropertyNamesType, FieldAttributes.Public); /* * 構造函數 實例化 ModifiedPropertyNames,生成類似於下面的代碼 ModifiedPropertyNames = new List<string>(); */ ConstructorBuilder constructorBuilder = typeBuilderProxy.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null); ILGenerator ilgCtor = constructorBuilder.GetILGenerator(); ilgCtor.Emit(OpCodes.Ldarg_0);//加載當前類 ilgCtor.Emit(OpCodes.Newobj, modifiedPropertyNamesType.GetConstructor(new Type[0]));//實例化對象入棧 ilgCtor.Emit(OpCodes.Stfld, fbModifiedPropertyNames);//設置fbModifiedPropertyNames值,為剛入棧的實例化對象 ilgCtor.Emit(OpCodes.Ret);//返回 //獲取被代理對象的所有屬性,循環屬性進行重寫 PropertyInfo[] properties = typeNeedProxy.GetProperties(); foreach (PropertyInfo propertyInfo in properties) { string propertyName = propertyInfo.Name; Type typePepropertyInfo = propertyInfo.PropertyType; //動態創建字段和屬性 FieldBuilder fieldBuilder = typeBuilderProxy.DefineField("_" + propertyName, typePepropertyInfo, FieldAttributes.Private); PropertyBuilder propertyBuilder = typeBuilderProxy.DefineProperty(propertyName, PropertyAttributes.SpecialName, typePepropertyInfo, null); //重寫屬性的Get Set方法 var methodGet = typeBuilderProxy.DefineMethod("get_" + propertyName, GetSetMethodAttributes, typePepropertyInfo, Type.EmptyTypes); var methodSet = typeBuilderProxy.DefineMethod("set_" + propertyName, GetSetMethodAttributes, null, new Type[] { typePepropertyInfo }); //il of get method var ilGetMethod = methodGet.GetILGenerator(); ilGetMethod.Emit(OpCodes.Ldarg_0); ilGetMethod.Emit(OpCodes.Ldfld, fieldBuilder); ilGetMethod.Emit(OpCodes.Ret); //il of set method ILGenerator ilSetMethod = methodSet.GetILGenerator(); ilSetMethod.Emit(OpCodes.Ldarg_0); ilSetMethod.Emit(OpCodes.Ldarg_1); ilSetMethod.Emit(OpCodes.Stfld, fieldBuilder); ilSetMethod.Emit(OpCodes.Ldarg_0); ilSetMethod.Emit(OpCodes.Ldfld, fbModifiedPropertyNames); ilSetMethod.Emit(OpCodes.Ldstr, propertyInfo.Name); ilSetMethod.Emit(OpCodes.Callvirt, modifiedPropertyNamesType.GetMethod("Add", new Type[] { typeof(string) })); ilSetMethod.Emit(OpCodes.Pop); ilSetMethod.Emit(OpCodes.Ret); //設置屬性的Get Set方法 propertyBuilder.SetGetMethod(methodGet); propertyBuilder.SetSetMethod(methodSet); } //使用動態類創建類型 Type proxyClassType = typeBuilderProxy.CreateType(); //保存動態創建的程序集 if(isSavaDynamicModule) assemblyBuilder.Save(DynamicModuleDllName); //創建類實例 var instance = Activator.CreateInstance(proxyClassType); return (T)instance; } /// <summary> /// 獲取屬性的變更名稱, /// 此處只檢測調用了Set方法的屬性,不會檢測值是否真的有變 /// </summary> /// <param name="obj"></param> /// <returns></returns> public static HashSet<string> GetModifiedProperties(object obj) { FieldInfo fieldInfo = obj.GetType().GetField(ModifiedPropertyNamesFieldName); if (fieldInfo == null) return null; object value = fieldInfo.GetValue(obj); return value as HashSet<string>; }
調用生成器的代碼如:
[TestMethod] public void TestCreateDynamicProxy() { User user = DynamicProxyGenerator.CreateDynamicProxy<User>(); user.LoginName = "login name"; user.LoginName = "login name"; user.Password = "login name"; HashSet<string> modifiedPropertyNames = DynamicProxyGenerator.GetModifiedProperties(user) as HashSet<string>; Assert.IsNotNull(modifiedPropertyNames); Assert.AreEqual(2, modifiedPropertyNames.Count); Assert.IsTrue(modifiedPropertyNames.Any(p => p == "LoginName")); Assert.IsTrue(modifiedPropertyNames.Any(p => p == "Password")); }
當然此代理器並沒有解決ORM更新部分字段的問題,需要根據不同的ORM去實現,既然已經知道了哪些屬性變更了,就很容易了,比如EF中可以把DbContext轉化為ObjectContext,然后設置需要更新的屬性名,代碼如下:
dbContext.Set<TEntity>().Attach(entity); var stateEntry = return (IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity); foreach (string propertyName in modifiedProperty) { stateEntry.SetModifiedProperty(propertyName); }
生成的代理類繼承傳入的實體並重寫實體的屬性(屬性必須是Virtual的),重寫了Set方法,當調用Set方法時就向HashSet<string>中插入屬性名(如果想獲取屬性值,請修改Set方法的IL),並沒有比較原始值和最新值是否有值的變更,只要調用了Set方法,就把此屬性名加入到屬性變更列表中
就寫到這里了,有不對的地方歡迎各位指正。
代理類的源代碼和測試程序下載地址為:http://files.cnblogs.com/why520crazy/DynamicProxyDemo.rar
前面說的受啟發的博主是 http://www.cnblogs.com/daxnet/archive/2012/04/16/2452660.html