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 方法。

4

8 回答 8

28

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

让我们翻转一下,以这段代码为例:

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方法(如果它实现了该接口)。

于 2010-03-05T15:56:24.373 回答
20

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

于 2010-03-05T16:14:01.133 回答
11

你可能想看看微软的 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);
}
于 2010-03-05T15:51:21.243 回答
3

这是一个简短的 LINQ 版本,它扩展了 object 并返回了一个不相等的属性列表:

用法: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; }
    }    
}
于 2017-06-26T13:02:07.463 回答
2

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

想象一下这个场景:

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

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

于 2012-05-17T23:22:03.020 回答
1

Liviu Trifoi 解决方案:使用 CompareNETObjects 库。 GitHub - NuGet 包-教程

于 2018-01-09T13:08:31.580 回答
0

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

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

于 2010-03-05T15:55:35.233 回答
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; }
}
于 2018-02-06T20:39:01.260 回答