查找两个 C# 对象之间的属性差异 [英] Finding property differences between two C# objects

查看:32
本文介绍了查找两个 C# 对象之间的属性差异的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

public static IList GenerateAuditLogMessages(T originalObject, T changedObject){IList list = new List();string className = string.Concat("[", originalObject.GetType().Name, "] ");foreach(originalObject.GetType().GetProperties() 中的PropertyInfo 属性){类型可比 =property.PropertyType.GetInterface("System.IComparable");如果(可比!= null){字符串 originalPropertyValue =property.GetValue(originalObject, null) 作为字符串;字符串 newPropertyValue =property.GetValue(changedObject, null) 作为字符串;if (originalPropertyValue != newPropertyValue){list.Add(string.Concat(className, property.Name," 由 '", originalPropertyValue,"' 到 '", newPropertyValue, "'"));}}}退货清单;}

我正在寻找 System.IComparable,因为所有数字类型(例如 Int32 和 Double)都实现了 IComparable,String、Char 和 DateTime 也是如此."这似乎是查找任何非自定义类的属性的最佳方式.

利用 WCF 或 Web 服务代理代码生成的 PropertyChanged 事件听起来不错,但没有为我的审核日志(旧值和新值)提供足够的信息.

正在寻找有关是否有更好的方法可以做到这一点的意见,谢谢!

@Aaronaught,这里是一些基于执行 object.Equals 生成正匹配的示例代码:

地址 address1 = new Address();address1.StateProvince = new StateProvince();地址 address2 = new Address();address2.StateProvince = new StateProvince();IList list = Utility.GenerateAuditLogMessages(address1, address2);

<块引用>

"[地址] StateProvince 由'MyAccountService.StateProvince' 到'MyAccountService.StateProvince'"

它是 StateProvince 类的两个不同实例,但属性的值是相同的(在本例中均为 null).我们不会覆盖 equals 方法.

解决方案

IComparable 用于排序比较.要么使用 IEquatable,要么只使用静态的 System.Object.Equals 方法.如果对象不是原始类型但仍然通过覆盖 Equals 定义自己的相等比较,后者的好处也可以工作.

object originalValue = property.GetValue(originalObject, null);object newValue = property.GetValue(changedObject, null);if (!object.Equals(originalValue, newValue)){字符串 originalText = (originalValue != null) ?originalValue.ToString() : "[NULL]";字符串 newText = (newText != null) ?newValue.ToString() : "[NULL]";//等等.}

这显然并不完美,但如果您只使用您控制的类来执行此操作,那么您可以确保它始终满足您的特定需求.

还有其他方法来比较对象(例如校验和、序列化等),但如果类没有始终如一地实现 IPropertyChanged 并且您想真正了解差异.

<小时>

更新新的示例代码:

地址 address1 = new Address();address1.StateProvince = new StateProvince();地址 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");地址 address2 = new Address("35 Elm St");address2.StateProvince = new StateProvince("AZ");

这些应该被认为是平等的吗?好吧,他们会使用你的方法,因为 StateProvince 没有实现 IComparable.这是您的方法报告两个对象在原始情况下相同的唯一原因.由于 StateProvince 类没有实现 IComparable,跟踪器只是完全跳过该属性.但这两个地址显然不相等!

这就是我最初建议使用 object.Equals 的原因,因为这样你就可以在 StateProvince 方法中覆盖它以获得更好的结果:

公共类StateProvince{公共字符串代码 { 获取;放;}公共覆盖布尔等于(对象 obj){如果(对象 == 空)返回假;StateProvince sp = obj as StateProvince;if (object.ReferenceEquals(sp, null))返回假;返回(sp.Code == 代码);}public bool Equals(StateProvince sp){if (object.ReferenceEquals(sp, null))返回假;返回(sp.Code == 代码);}公共覆盖 int GetHashCode(){返回代码.GetHashCode();}公共覆盖字符串 ToS​​tring(){return string.Format("代码:[{0}]",代码);}}

完成此操作后,object.Equals 代码将完美运行.与其天真地检查 address1address2 是否具有相同的 StateProvince 引用,它实际上会检查语义是否相等.

<小时>

解决此问题的另一种方法是扩展跟踪代码以实际下降到子对象中.换句话说,对于每个属性,检查 Type.IsClass 和可选的 Type.IsInterface 属性,如果 true,则递归调用属性本身的更改跟踪方法,在递归返回的任何审计结果前加上属性名称.因此,您最终会更改 StateProvinceCode.

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

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

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.

解决方案

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.

这篇关于查找两个 C# 对象之间的属性差异的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆