【问题标题】:Finding property differences between two C# objects查找两个 C# 对象之间的属性差异
【发布时间】:2026-02-02 13:40:02
【问题描述】:

我正在处理的项目需要一些简单的审核日志记录,以便在用户更改电子邮件、帐单地址等时进行。我们正在使用的对象来自不同的来源,一个是 WCF 服务,另一个是 Web服务。

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

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 和 Double)都实现了 IComparable,String、Char 和 DateTime 也是如此。”这似乎是查找任何不是自定义类的属性的最佳方式。

利用 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);

"[Address] StateProvince 从 'MyAccountService.StateProvince' 到 'MyAccountService.StateProvince'"

它是 StateProvince 类的两个不同实例,但属性的值是相同的(在这种情况下都是 null)。我们没有覆盖 equals 方法。

【问题讨论】:

    标签: c# .net reflection auditing


    【解决方案1】:

    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 在这两种情况下都可能为空,但address1address2StateProvince 属性仍然具有非空值,并且每个实例都不同。因此,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

    我有时也使用上述方法,但在您想要比较语义相等性(即审计)的对象上覆盖 Equals 并提供适当的 ToString 覆盖更容易,这样可以清楚地知道发生了什么变化。它不适用于深度嵌套,但我认为想要以这种方式进行审计是不寻常的。

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

    【讨论】:

    • 不幸的是,只使用 object.Equals 为引用类型返回 true,例如:[Address] StateProvince 从 'MyAccountService.StateProvince' 更改为 'MyAccountService.StateProvince'
    • @Pete Nelson:假设您实际上是在比较不同的参考文献,那是……不可能的。我们可以看到一个完整的例子吗?它会覆盖Equals 方法吗?我使用与此非常相似的代码,它从不给出假阴性。
    • 在原帖中添加了匹配示例。
    • 这对于大多数情况来说绝对是一个可行的解决方案。但是,我们正在使用 WCF 和 Web 服务代理类,因此实现接口或覆盖 equals 并不那么简单。由于生成的代理类被声明为部分类,我们可以添加额外的代码,这并非不可能。不过,我肯定会检查 Type.IsClass 属性,看看我是否想深入比较整个对象图。
    • 您可以将string newText = (newText != null) ? 更改为string newText = (newValue != null) ?,因为它无法编译。
    【解决方案2】:

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

    【讨论】:

    • 通过一些粗略的测试,这看起来也不错。至少,他们的源代码为我提供了更多关于他们如何进行对象比较的信息。
    • 这是另一个使用表达式树的。可能更快。 github.com/StevenGilligan/AutoCompare
    【解决方案3】:

    你可能想看看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);
    }
    

    【讨论】:

      【解决方案4】:

      这是一个简短的 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; }
          }    
      }
      

      【讨论】:

      • 虽然此代码可能会回答问题,但提供有关它如何和/或为什么解决问题的额外上下文将提高​​答案的长期价值。请阅读此how-to-answer 以提供高质量的答案。
      • 这仅适用于基本属性类型。子对象的列表将始终报告为不同,即使它们相同。
      【解决方案5】:

      您永远不想在可变属性(可能由某人更改的属性)上实现 GetHashCode - 即非私有设置器。

      想象一下这个场景:

      1. 您将对象的实例放入使用GetHashCode()“幕后”或直接使用(哈希表)的集合中。
      2. 然后有人更改了您在 GetHashCode() 实现中使用的字段/属性的值。

      猜猜看...您的对象在集合中永久丢失,因为集合使用GetHashCode() 来查找它!您已经有效地更改了最初放置在集合中的哈希码值。可能不是你想要的。

      【讨论】:

        【解决方案6】:

        Liviu Trifoi solution: 使用 CompareNETObjects 库。 GitHub - NuGet package - Tutorial.

        【讨论】:

          【解决方案7】:

          我认为这种方法非常简洁,它避免了重复或向类添加任何内容。你还在寻找什么?

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

          【讨论】:

            【解决方案8】:

            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; }
            }
            

            【讨论】: