需求比较复杂,真心不想多说。每次数据库操作都要记录原始值和新值到Audit表。而且其中一些外键字段不能存外键值而必须存其业务对应值,也就是其对应的导航属性的某个值
既然用了EF,哪个属性是普通属性,哪个是导航属性,哪个属性能对应到导航属性,都是可以得到的不是问题。但最终记录的是哪个导航属性的哪个值就不好说了,只能上配置信息。另外就是app规定不需要延迟加载,于是需要手动加载导航属性值
先定义一下配置信息相关类
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace EFAuditComponet{ public class AuditConfigItem { public string EntityTypeName { set; get; } public string EntityPropertyName { set; get; } public string TargetNavigationPropertyPath { set; get; } public bool NeedToRecordAudit { set; get; } } public static class AuditConfigExtention { public static bool CheckContains(this Listlist,string entityTypeFullName, string propertyName) { var result = list.GetConfigItem(entityTypeFullName, propertyName); if (result == null) return false; else return true; } public static AuditConfigItem GetConfigItem(this List list, string entityTypeFullName, string propertyName) { return list.Find(p => p.EntityTypeName == entityTypeFullName && p.EntityPropertyName == propertyName); } }}
没啥好说的,POCO+扩展方法,简单好用。需要一提的是TargetNavigationPropertyPath的格式,是属性名.属性名.属性名....属性名。前面的都是导航属性的属性名,最后是实际值的属性名。具体用法后面示例中有。
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace EFAuditComponet{ public class EFAuditConfig { public static ListConfigData = new List (); }}
将配置信息存于一个静态List中,写个类是为了方便将来扩展
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace EFAuditComponet{ public class AuditRecord { public string PropertyName { set; get; } public string OldValue { set; get; } public string NewValue { set; get; } }}
这个也没啥好说的,Audit记录POCO
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Data.Entity;using System.Data.Entity.Infrastructure;using System.Data.Objects;using System.Data.Metadata.Edm;using System.Collections;namespace EFAuditComponet{ public class EFAuditManager { public void GetNavigationProperty(DbContext context) where T : class { if (context == null) { throw new ArgumentNullException("context"); } ObjectContext objContext = ((IObjectContextAdapter)context).ObjectContext; var set = objContext.CreateObjectSet (); var entitySet = set.EntitySet; Console.WriteLine("类型名称:"); Console.WriteLine(entitySet.ElementType.FullName); Console.WriteLine(entitySet.ElementType.Name); Console.WriteLine("属性名称:"); foreach (var pro in entitySet.ElementType.Properties) { Console.WriteLine(pro.Name); } Console.WriteLine("导航属性名称:"); foreach (var pro in entitySet.ElementType.NavigationProperties) { Console.WriteLine(pro.Name); } Console.WriteLine("成员名称:"); foreach (var pro in entitySet.ElementType.Members) { Console.WriteLine(pro.Name); } Console.WriteLine("属性与对应导航属性:"); foreach (var pro in entitySet.ElementType.NavigationProperties) { foreach (var fpro in pro.GetDependentProperties()) { Console.WriteLine(fpro.Name + ":" + pro.ToEndMember.Name); } } Console.ReadKey(); } public List GetObjectDataAndIncludeDataByConfig (DbContext context, T objInstance) where T : class { List auditList = new List (); ObjectContext objContext = ((IObjectContextAdapter)context).ObjectContext; var set = objContext.CreateObjectSet (); var entitySet = set.EntitySet; Dictionary naviCollection = new Dictionary (); foreach (var pro in entitySet.ElementType.NavigationProperties) { foreach (var fpro in pro.GetDependentProperties()) { naviCollection.Add(fpro.Name, pro); } } foreach (var pro in entitySet.ElementType.Properties) { string propertyName = pro.Name; AuditConfigItem configItem = EFAuditConfig.ConfigData.GetConfigItem(entitySet.ElementType.Name, propertyName); if (configItem != null && !configItem.NeedToRecordAudit) { continue; } AuditRecord record = new AuditRecord(); DbEntityEntry entry = Attach (context, objInstance); //DbPropertyValues dbValues = entry.GetDatabaseValues(); object originalValue = entry.Property(propertyName).OriginalValue; object currentValue = entry.Property(propertyName).CurrentValue; if (configItem == null || string.IsNullOrEmpty(configItem.TargetNavigationPropertyPath)) { record.PropertyName = propertyName; record.OldValue = originalValue.ToString(); record.NewValue = currentValue.ToString(); } else { record.PropertyName = propertyName; entry.Property(propertyName).CurrentValue = originalValue; record.OldValue = LoadReferenceValue(context,entry, configItem.TargetNavigationPropertyPath); entry.Property(propertyName).CurrentValue = currentValue; record.NewValue = LoadReferenceValue(context,entry, configItem.TargetNavigationPropertyPath); } auditList.Add(record); } return auditList; } private string LoadReferenceValue(DbContext context,DbEntityEntry entry,string targetNavigationPropertyPath) { string returnValue = null; string[] propertyNames = targetNavigationPropertyPath.Split('.'); for (int i = 0; i < propertyNames.Length; i++) { if (i != propertyNames.Length - 1) { if (!entry.Reference(propertyNames[i]).IsLoaded) entry.Reference(propertyNames[i]).Load(); entry = context.Entry(entry.Reference(propertyNames[i]).CurrentValue); } else returnValue = entry.Property(propertyNames[i]).CurrentValue == null ? null : entry.Property(propertyNames[i]).CurrentValue.ToString(); } return returnValue; } public DbEntityEntry Attach (DbContext context, T obj) where T : class { if (context == null) { throw new ArgumentNullException("context"); } DbEntityEntry entry; try { entry =context.Entry (obj); entry.State = System.Data.EntityState.Modified; } catch { var set = ((IObjectContextAdapter)context).ObjectContext.CreateObjectSet (); var entitySet = set.EntitySet; string[] keyNames = entitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); string precidate = string.Empty; foreach (string filter in keyNames) { precidate += "it." + filter + "=" + context.Entry (obj).Property(filter).CurrentValue.ToString(); } var entity = set.Where(precidate).First(); //context.ChangeTracker.Entries ().ToList().Remove(GetEntityKeyNames(context, pE); entry = context.Entry (entity); entry.CurrentValues.SetValues(obj); } return entry; } }}
这个是真正的核心类,提供了所有我们需要的方法。方法名乱起了,其实第一个方法教你如何获得普通属性,如何获得导航属性,如何获得普通属性与导航属性的对应。
可以对照代码看看结果图,用PatronIdentification对象做的示例:
GetObjectDataAndIncludeDataByConfig则是真正的能将一个对象与Context中的对象比较,并得到AduitRecord集合。
这是修改了一个查询得到的对象后进行比对得到结果:
using (BE50Beta_AdminEntities1 context = new BE50Beta_AdminEntities1()) { PatronIdentification result = context.PatronIdentification.Where(p => p.IdentificatinNumber == "aaaa").First(); result.CountryID = 2; result.IdentificatinNumber = "1234567"; EFAuditManager manager = new EFAuditManager(); Listlist = manager.GetObjectDataAndIncludeDataByConfig (context, result); foreach (var record in list) { Console.WriteLine(record.PropertyName + " OldValue:" + record.OldValue + " NewValue:" + record.NewValue); } Console.ReadKey(); }
思路其实也很简单,遍历一个实体的普通属性,如果针对每一个属性,有一个配置信息存在且该配置信息的TargetNavigationPropertyPath不为空。那么就需要记录TargetNavigationPropertyPath所指的属性值而不是本身值
记录本身值无难度,难点在记录导航属性的值,可能会导航多次。
于是用了LoadReferenceValue,遍历TargetNavigationPropertyPath路径,在最后一个路径点去取出真正的值,之前只是将导航属性对象手动Load。
以下是模拟了有配置数据的情况,注意TargetNavigationPropertyPath的用法
EFAuditConfig.ConfigData.Add(new AuditConfigItem() { EntityTypeName = "PatronIdentification", EntityPropertyName = "CountryID", NeedToRecordAudit = true, TargetNavigationPropertyPath = "Country.CountryName" }); using (BE50Beta_AdminEntities1 context = new BE50Beta_AdminEntities1()) { PatronIdentification result = context.PatronIdentification.Where(p => p.IdentificatinNumber == "aaaa").First(); result.CountryID = 2; result.IdentificatinNumber = "1234567"; EFAuditManager manager = new EFAuditManager(); Listlist = manager.GetObjectDataAndIncludeDataByConfig (context, result); foreach (var record in list) { Console.WriteLine(record.PropertyName + " OldValue:" + record.OldValue + " NewValue:" + record.NewValue); } Console.ReadKey(); }
可以看到CountryID被成功替换成CountryName