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

发布于 2024-08-24 04:45:59 字数 1870 浏览 3 评论 0原文

我正在开发的项目需要在用户更改电子邮件、帐单地址等时进行一些简单的审核日志记录。我们正在使用的对象来自不同的来源,一个是 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 方法。

The project I'm working on needs some simple audit logging for when a user changes their email, billing address, etc. The objects we're working with are coming from different sources, one a WCF service, the other a web service.

I've implemented the following method using reflection to find changes to the properties on two different objects. This generates a list of the properties that have differences along with their old and new values.

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

I'm looking for System.IComparable because "All numeric types (such as Int32 and Double) implement IComparable, as do String, Char, and DateTime." This seemed the best way to find any property that's not a custom class.

Tapping into the PropertyChanged event that's generated by the WCF or web service proxy code sounded good but doesn't give me enough info for my audit logs (old and new values).

Looking for input as to if there is a better way to do this, thanks!

@Aaronaught, here is some example code that is generating a positive match based on doing 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 changed from
'MyAccountService.StateProvince' to
'MyAccountService.StateProvince'"

It's two different instances of the StateProvince class, but the values of the properties are the same (all null in this case). We're not overriding the equals method.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

魄砕の薆 2024-08-31 04:46:00

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

The my way of Expression tree compile version. It should faster than 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; }
}
停顿的约定 2024-08-31 04:45:59

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 仍然具有非空值code> 属性并且每个实例都是不同的。因此,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,它采用相同类型的第二个实例作为参数,并实际上返回所有差异的列表(或可枚举)。它类似于上面重写的 object.Equals 方法,但返回更多信息。当对象图非常复杂并且您知道不能依赖反射或Equals时,这非常有用。您可以将其与上述方法结合起来;实际上,您所要做的就是用 IComparable 替换您的 IAuditable 并调用 Audit 方法(如果它实现了该接口)。

IComparable is for ordering comparisons. Either use IEquatable instead, or just use the static System.Object.Equals method. The latter has the benefit of also working if the object is not a primitive type but still defines its own equality comparison by overriding 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.
}

This obviously isn't perfect, but if you're only doing it with classes that you control, then you can make sure it always works for your particular needs.

There are other methods to compare objects (such as checksums, serialization, etc.) but this is probably the most reliable if the classes don't consistently implement IPropertyChanged and you want to actually know the differences.


Update for new example code:

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

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

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

The reason that using object.Equals in your audit method results in a "hit" is because the instances are actually not equal!

Sure, the StateProvince may be empty in both cases, but address1 and address2 still have non-null values for the StateProvince property and each instance is different. Therefore, address1 and address2 have different properties.

Let's flip this around, take this code as an example:

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

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

Should these be considered equal? Well, they will be, using your method, because StateProvince does not implement IComparable. That's the only reason why your method reported that the two objects were the same in the original case. Since the StateProvince class does not implement IComparable, the tracker just skips that property entirely. But these two addresses are clearly not equal!

This is why I originally suggested using object.Equals, because then you can override it in the StateProvince method to get better results:

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

Once you've done this, the object.Equals code will work perfectly. Instead of naïvely checking whether or not address1 and address2 literally have the same StateProvince reference, it will actually check for semantic equality.


The other way around this is to extend the tracking code to actually descend into sub-objects. In other words, for each property, check the Type.IsClass and optionally the Type.IsInterface property, and if true, then recursively invoke the change-tracking method on the property itself, prefixing any audit results returned recursively with the property name. So you'd end up with a change for StateProvinceCode.

I use the above approach sometimes too, but it's easier to just override Equals on the objects for which you want to compare semantic equality (i.e. audit) and provide an appropriate ToString override that makes it clear what changed. It doesn't scale for deep nesting but I think it's unusual to want to audit that way.

The last trick is to define your own interface, say IAuditable<T>, which takes a second instance of the same type as a parameter and actually returns a list (or enumerable) of all of the differences. It's similar to our overridden object.Equals method above but gives back more information. This is useful for when the object graph is really complicated and you know you can't rely on Reflection or Equals. You can combine this with the above approach; really all you have to do is substitute IComparable for your IAuditable and invoke the Audit method if it implements that interface.

池木 2024-08-31 04:45:59

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

You might want to look at Microsoft's Testapi It has an object comparison api that does deep comparisons. It might be overkill for you but it could be worth a look.

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);
}
趁年轻赶紧闹 2024-08-31 04:45:59

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

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

Here a short LINQ version that extends object and returns a list of properties that are not equal:

usage: 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; }
    }    
}
你不是我要的菜∠ 2024-08-31 04:45:59

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

想象一下这样的场景:

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

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

You never want to implement GetHashCode on mutable properties (properties that could be changed by someone) - i.e. non-private setters.

Imagine this scenario:

  1. you put an instance of your object in a collection which uses GetHashCode() "under the covers" or directly (Hashtable).
  2. Then someone changes the value of the field/property that you've used in your GetHashCode() implementation.

Guess what... your object is permanently lost in the collection since the collection uses GetHashCode() to find it! You've effectively changed the hashcode value from what was originally placed in the collection. Probably not what you wanted.

我也只是我 2024-08-31 04:45:59

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

Liviu Trifoi solution: Using CompareNETObjects library.
GitHub - NuGet package - Tutorial.

绝對不後悔。 2024-08-31 04:45:59

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

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

I think this method is quite neat, it avoids repetition or adding anything to classes. What more are you looking for?

The only alternative would be to generate a state dictionary for the old and new objects, and write a comparison for them. The code for generating the state dictionary could reuse any serialisation you have for storing this data in the database.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文