在两个C#对象之间查找属性差异

58
我正在处理的项目需要进行简单的审计日志记录,以便在用户更改其电子邮件、账单地址等时进行记录。我们正在处理来自不同来源的对象,其中一个是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);

"[地址] StateProvince 从 'MyAccountService.StateProvince' 变为 'MyAccountService.StateProvince'"

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

8个回答

29

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在两种情况下都可能为空,但是对于StateProvince属性,address1address2仍具有非空值,并且每个实例都不同。因此,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 覆盖更容易。虽然这不适用于深层嵌套,但我认为很少有人想要这样进行审核。
最后一个技巧是定义自己的接口,例如 IAuditable<T>,它需要一个同类型的第二个实例作为参数,并实际返回所有差异的列表(或可枚举)。这类似于我们上面重写的 object.Equals 方法,但提供了更多信息。当对象图非常复杂并且您知道不能依赖反射或 Equals 时,这很有用。您可以将其与上述方法相结合;只需将 IComparable 替换为您的 IAuditable,如果它实现了该接口,则调用 Audit 方法即可。

不幸的是,仅使用object.Equals对于引用类型返回true,例如:[地址] 州省已从 'MyAccountService.StateProvince' 更改为 'MyAccountService.StateProvince' - Pete Nelson
@Pete Nelson:假设您实际上是在比较不同的引用,那是不太可能的。我们能看到一个完整的带有类的示例吗?它是否重写了Equals方法?我使用的代码与此非常相似,从来没有出现过错误的负值。 - Aaronaught
在原帖中添加了一个匹配的示例。 - Pete Nelson
这绝对是大多数情况下可行的解决方案。然而,我们正在使用WCF和Web服务代理类,因此实现接口或覆盖equals并不那么直截了当。由于生成的代理类被声明为部分类,并且我们可以添加附加代码,所以这并非不可能。我一定会检查Type.IsClass属性,看看是否要深入比较整个对象图。 - Pete Nelson
你可以将代码中的 string newText = (newText != null) ? 改为 string newText = (newValue != null) ?,因为前者会导致编译错误。 - gooleem

21

这个在Github上的项目可以检查几乎任何类型的属性,并且可以按照您的需求进行自定义。


从一些初步的测试来看,这看起来也相当不错。至少,他们的源代码让我更加了解他们如何进行对象比较。 - Pete Nelson
这是另一个使用表达式树的示例。可能更快。https://github.com/StevenGilligan/AutoCompare - Damien Sawyer

11
你可以看一下Microsoft的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);
}

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

1
虽然这段代码可能回答了问题,但提供关于它如何和/或为什么解决问题的附加上下文将会提高答案的长期价值。请阅读此 如何回答 页面,以提供高质量的答案。 - thewaywewere
这仅适用于基本属性类型。子对象列表即使相同,也始终会报告为不同。 - Alan Barber

2

你永远不应该在可变属性上(即非私有 setter)实现 GetHashCode -

想象一下这种情况:

  1. 你将对象实例放入一个使用 GetHashCode()“底层”或直接使用 Hashtable 的集合中。
  2. 然后某人更改了您在 GetHashCode() 实现中使用的字段/属性的值。

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


1

0

我的表达式树编译版本。它应该比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; }
}

0

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

唯一的替代方案是为旧对象和新对象生成状态字典,并为它们编写比较代码。生成状态字典的代码可以重用您用于将此数据存储在数据库中的序列化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接