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

时间:2010-03-05 15:46:22

标签: c# .net reflection auditing

我正在处理的项目需要一些简单的审核日志记录,以便用户更改其电子邮件,帐单地址等。我们正在使用的对象来自不同的来源,一个是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 个答案:

答案 0 :(得分:25)

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并且您想要真正了解差异,那么这可能是最可靠的。< / p>


更新新示例代码:

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进行更改。

我有时也会使用上面的方法,但是对于要比较语义相等(即审计)的对象重写Equals并提供适当的ToString覆盖更容易明确改变了什么。它不适用于深度嵌套,但我认为想要以这种方式进行审核是不寻常的。

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

答案 1 :(得分:18)

codeplex上的

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

答案 2 :(得分:10)

您可能希望查看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);
}

答案 3 :(得分: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; }
        }

    }

答案 4 :(得分:2)

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

想象一下这种情况:

  1. 您将对象的实例放在一个集合中,该集合使用GetHashCode()“隐藏”或直接(Hashtable)。
  2. 然后有人更改您在GetHashCode()实现中使用的字段/属性的值。
  3. 猜猜是什么......你的对象在集合中永久丢失,因为集合使用GetHashCode()来查找它!您已经有效地更改了最初放置在集合中的哈希码值。可能不是你想要的。

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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

答案 7 :(得分: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; }
}