2010-03-05 58 views
45

我正在处理的项目需要一些简单的审核日志记录,以便用户更改其电子邮件地址,帐单地址等。我们正在处理的对象来自不同的来源,一个WCF服务,另一个Web服务。查找两个C#对象之间的属性差异

我已经实现了以下使用反射的方法来查找对两个不同对象的属性所做的更改。这会生成与旧值和新值有差异的属性列表。

public static IList GenerateAuditLogMessages(T originalObject, T changedObject) 
{ 
    IList list = new List(); 
    string className = string.Concat("[", originalObject.GetType().Name, "] "); 

    foreach (PropertyInfo property in originalObject.GetType().GetProperties()) 
    { 
     Type comparable = 
      property.PropertyType.GetInterface("System.IComparable"); 

     if (comparable != null) 
     { 
      string originalPropertyValue = 
       property.GetValue(originalObject, null) as string; 
      string newPropertyValue = 
       property.GetValue(changedObject, null) as string; 

      if (originalPropertyValue != newPropertyValue) 
      { 
       list.Add(string.Concat(className, property.Name, 
        " changed from '", originalPropertyValue, 
        "' to '", newPropertyValue, "'")); 
      } 
     } 
    } 

    return list; 
}

我在寻找System.IComparable,因为“所有的数字类型(如的Int32和双人间)实现IComparable,因为这样做字符串,字符和日期时间。”这似乎是找到任何不属于自定义类的财产的最佳方式。

攻击由WCF或Web服务代理代码生成的PropertyChanged事件听起来不错,但没有为我的审计日志(旧值和新值)提供足够的信息。

寻找关于是否有更好的方式来做到这一点的意见,谢谢!

@Aaronaught,这里是基于这样做的Object.Equals产生正匹配一些示例代码:

Address address1 = new Address(); 
address1.StateProvince = new StateProvince(); 

Address address2 = new Address(); 
address2.StateProvince = new StateProvince(); 

IList list = Utility.GenerateAuditLogMessages(address1, address2);

“[地址] StateProvince从 'MyAccountService.StateProvince' 改为 ' MyAccountService.StateProvince'“

它是StateProvince类的两个不同实例,但属性的值是相同的(在这种情况下全为null)。我们并不是重写equals方法。

回答

22

IComparable用于排序比较。改为使用IEquatable,或者只使用静态System.Object.Equals方法。如果对象不是原始类型,后者也有效,但仍然通过覆盖Equals来定义自己的相等比较。

object originalValue = property.GetValue(originalObject, null); 
object newValue = property.GetValue(changedObject, null); 
if (!object.Equals(originalValue, newValue)) 
{ 
    string originalText = (originalValue != null) ? 
     originalValue.ToString() : "[NULL]"; 
    string newText = (newText != null) ? 
     newValue.ToString() : "[NULL]"; 
    // etc. 
} 

这显然不是完美的,但如果你只是用你控制的类来做,那么你可以确保它始终满足你的特定需求。

还有其他方法可以比较对象(如校验和,序列化等),但如果类不一致地实现IPropertyChanged并且您想实际知道差异,则这可能是最可靠的。


更新新的示例代码:

Address address1 = new Address(); 
address1.StateProvince = new StateProvince(); 

Address address2 = new Address(); 
address2.StateProvince = new StateProvince(); 

IList list = Utility.GenerateAuditLogMessages(address1, address2); 

的原因是,在“命中”用你的方法,审计结果object.Equals是因为情况实际上是不相等的!

诚然,StateProvince可以在两种情况下是空的,但address1address2仍能为StateProvince财产上的非空值,而且每个实例是不同的。因此,address1address2具有不同的属性。

让我们翻转过来,这一点,以此代码为例:

Address address1 = new Address("35 Elm St"); 
address1.StateProvince = new StateProvince("TX"); 

Address address2 = new Address("35 Elm St"); 
address2.StateProvince = new StateProvince("AZ"); 

如果这些被认为是平等的吗?那么,他们会用你的方法,因为StateProvince没有实现IComparable。这就是为什么你的方法报告两个对象在原始情况下是相同的唯一原因。由于StateProvince类不执行IComparable,跟踪器只是完全跳过该属性。但是这两个地址显然不相等!

这就是为什么我使用object.Equals最初的建议,因为这样你可以在StateProvince方法来覆盖它,以获得更好的结果:

public class StateProvince 
{ 
    public string Code { get; set; } 

    public override bool Equals(object obj) 
    { 
     if (obj == null) 
      return false; 

     StateProvince sp = obj as StateProvince; 
     if (object.ReferenceEquals(sp, null)) 
      return false; 

     return (sp.Code == Code); 
    } 

    public bool Equals(StateProvince sp) 
    { 
     if (object.ReferenceEquals(sp, null)) 
      return false; 

     return (sp.Code == Code); 
    } 

    public override int GetHashCode() 
    { 
     return Code.GetHashCode(); 
    } 

    public override string ToString() 
    { 
     return string.Format("Code: [{0}]", Code); 
    } 
} 

一旦你做到了这一点,该object.Equals代码将很好地工作。它并不是天真地检查address1address2字面上是否具有相同的StateProvince引用,它实际上会检查语义相等。


另一种解决方法是扩展跟踪代码以实际下降到子对象。换句话说,对于每个属性,检查Type.IsClass和可选的Type.IsInterface属性,如果true,然后递归调用属性本身的变更跟踪方法,则以属性名称递归返回任何审计结果的前缀。所以你最终会改变为StateProvinceCode

我用上面的方法有时太,但它更容易只是覆盖上要比较语义平等(即审计),并提供适当的ToString覆盖,使得它清楚是什么改变了对象Equals。它不适合深层嵌套,但我认为想以这种方式进行审计是不寻常的。

最后一个技巧是定义你自己的接口,比如IAuditable<T>,它接受与参数相同类型的第二个实例,并实际返回所有差异的列表(或可枚举)。这与我们上面重写的object.Equals方法类似,但是会提供更多信息。这对于何时对象图非常复杂并且您知道不能依靠Reflection或Equals很有用。你可以将它与上面的方法结合起来;如果它实现了那个接口,你所要做的就是将IComparable替换为你的IAuditable并调用Audit方法。

+0

遗憾的是只使用的Object.Equals正在恢复为引用类型真实的,例如: [联系地址] StateProvince从“MyAccountService.StateProvince”到“MyAccountService.StateProvince” – 2010-03-05 19:14:03

+0

@Pete尼尔森改变:假设你实际上比较不同的参考文献,那是......不可能的。我们能否在课堂上看到一个完整的例子?它是否重写'Equals'方法?我使用的代码非常类似于此,它从不给出错误的否定。 – Aaronaught 2010-03-05 19:19:42

+0

为原始帖子添加了匹配的示例。 – 2010-03-05 19:44:41

9

你可能想看看Microsoft's Testapi它有一个深入比较的对象比较API。这可能是矫枉过正,但它可能值得一看。

var comparer = new ObjectComparer(new PublicPropertyObjectGraphFactory()); 
IEnumerable<ObjectComparisonMismatch> mismatches; 
bool result = comparer.Compare(left, right, out mismatches); 

foreach (var mismatch in mismatches) 
{ 
    Console.Out.WriteLine("\t'{0}' = '{1}' and '{2}'='{3}' do not match. '{4}'", 
     mismatch.LeftObjectNode.Name, mismatch.LeftObjectNode.ObjectValue, 
     mismatch.RightObjectNode.Name, mismatch.RightObjectNode.ObjectValue, 
     mismatch.MismatchType); 
} 
0

我觉得这个方法非常整齐,它避免了重复或向类中添加任何东西。你还想找什么?

唯一的选择是为旧对象和新对象生成状态字典,并为它们写一个比较。用于生成状态字典的代码可以重新使用用于将此数据存储在数据库中的任何序列化。

18

This codeplex上的项目几乎检查任何类型的属性,并且可以根据需要进行自定义。

+0

从一些粗略的测试来看,这看起来还不错。至少,他们的源代码给了我更多关于他们如何进行对象比较的信息。 – 2010-03-05 22:49:09

+0

谢谢你的链接thekaido,你为我节省了很多时间,我自己实现了一个类似的库:) – 2010-07-01 10:47:10

+0

这是另一个使用表达式树的。可能更快。 https://github.com/StevenGilligan/AutoCompare – 2017-09-12 23:41:48

2

你永远不想在可变属性(可能被某人改变的属性)上实现GetHashCode - 即非私人设置者。

想象一下这样的情景:

  1. 你把你的对象的实例,它使用的GetHashCode()“在幕后”或直接(哈希表)一个收藏。
  2. 然后有人更改您在GetHashCode()实现中使用的字段/属性的值。

猜猜是什么......由于集合使用GetHashCode()来查找它,因此您的对象永久丢失在集合中!您已经有效地改变了原始放置在集合中的hashcode值。可能不是你想要的。

2

这里延伸对象,并返回不相等属性的列表短LINQ版本:

用法:object.DetailedCompare(objectToCompare);

public static class ObjectExtensions 
    { 

     public static List<Variance> DetailedCompare<T>(this T val1, T val2) 
     { 
      var propertyInfo = val1.GetType().GetProperties(); 
      return propertyInfo.Select(f => new Variance 
       { 
        Property = f.Name, 
        ValueA = f.GetValue(val1), 
        ValueB = f.GetValue(val2) 
       }) 
       .Where(v => !v.ValueA.Equals(v.ValueB)) 
       .ToList(); 
     } 

     public class Variance 
     { 
      public string Property { get; set; } 
      public object ValueA { get; set; } 
      public object ValueB { get; set; } 
     } 

    } 
+0

尽管这段代码可能会回答这个问题,但提供关于如何解决问题和/或为何解决问题的额外上下文会提高答案的长期价值。请阅读此[如何回答](http://stackoverflow.com/help/how-to-answer)以提供高质量的答案。 – thewaywewere 2017-06-26 13:21:42

+0

这只适用于基本属性类型。即使相同,子对象列表也会始终报告不同。 – 2017-07-13 15:28:23

0

我的方式Expression树编译版本。它应该比PropertyInfo.GetValue快。

static class ObjDiffCollector<T> 
{ 
    private delegate DiffEntry DiffDelegate(T x, T y); 

    private static readonly IReadOnlyDictionary<string, DiffDelegate> DicDiffDels; 

    private static PropertyInfo PropertyOf<TClass, TProperty>(Expression<Func<TClass, TProperty>> selector) 
     => (PropertyInfo)((MemberExpression)selector.Body).Member; 

    static ObjDiffCollector() 
    { 
     var expParamX = Expression.Parameter(typeof(T), "x"); 
     var expParamY = Expression.Parameter(typeof(T), "y"); 

     var propDrName = PropertyOf((DiffEntry x) => x.Prop); 
     var propDrValX = PropertyOf((DiffEntry x) => x.ValX); 
     var propDrValY = PropertyOf((DiffEntry x) => x.ValY); 

     var dic = new Dictionary<string, DiffDelegate>(); 

     var props = typeof(T).GetProperties(); 
     foreach (var info in props) 
     { 
      var expValX = Expression.MakeMemberAccess(expParamX, info); 
      var expValY = Expression.MakeMemberAccess(expParamY, info); 

      var expEq = Expression.Equal(expValX, expValY); 

      var expNewEntry = Expression.New(typeof(DiffEntry)); 
      var expMemberInitEntry = Expression.MemberInit(expNewEntry, 
       Expression.Bind(propDrName, Expression.Constant(info.Name)), 
       Expression.Bind(propDrValX, Expression.Convert(expValX, typeof(object))), 
       Expression.Bind(propDrValY, Expression.Convert(expValY, typeof(object))) 
      ); 

      var expReturn = Expression.Condition(expEq 
       , Expression.Convert(Expression.Constant(null), typeof(DiffEntry)) 
       , expMemberInitEntry); 

      var expLambda = Expression.Lambda<DiffDelegate>(expReturn, expParamX, expParamY); 

      var compiled = expLambda.Compile(); 

      dic[info.Name] = compiled; 
     } 

     DicDiffDels = dic; 
    } 

    public static DiffEntry[] Diff(T x, T y) 
    { 
     var list = new List<DiffEntry>(DicDiffDels.Count); 
     foreach (var pair in DicDiffDels) 
     { 
      var r = pair.Value(x, y); 
      if (r != null) list.Add(r); 
     } 
     return list.ToArray(); 
    } 
} 

class DiffEntry 
{ 
    public string Prop { get; set; } 
    public object ValX { get; set; } 
    public object ValY { get; set; } 
}