查找两个 C# 对象之间的属性差异
我正在开发的项目需要在用户更改电子邮件、帐单地址等时进行一些简单的审核日志记录。我们正在使用的对象来自不同的来源,一个是 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
我的
Expression
树编译版本的方式。它应该比PropertyInfo.GetValue
更快。The my way of
Expression
tree compile version. It should faster thanPropertyInfo.GetValue
.IComparable
用于排序比较。可以使用IEquatable
代替,或者仅使用静态System.Object.Equals
方法。后者的好处是,如果对象不是原始类型,但仍然通过重写Equals
定义自己的相等比较,则也可以工作。这显然并不完美,但如果您只使用您控制的类来执行此操作,那么您可以确保它始终满足您的特定需求。
还有其他方法可以比较对象(例如校验和、序列化等),但如果类没有一致地实现 IPropertyChanged 并且您想真正了解差异,那么这可能是最可靠的。
更新新示例代码:
在审核方法中使用
object.Equals
导致“命中”的原因是实例实际上不相等!当然,在这两种情况下,
StateProvince
都可能为空,但address1
和address2
的StateProvince
仍然具有非空值code> 属性并且每个实例都是不同的。因此,address1
和address2
具有不同的属性。让我们反过来看,以这段代码为例:
这些应该被认为是相等的吗?好吧,他们会使用您的方法,因为
StateProvince
没有实现IComparable
。这是您的方法报告两个对象在原始情况下相同的唯一原因。由于StateProvince
类未实现IComparable
,因此跟踪器完全跳过该属性。但这两个地址显然不相等!这就是我最初建议使用
object.Equals
的原因,因为这样您就可以在StateProvince
方法中重写它以获得更好的结果:完成此操作后,
>object.Equals
代码将完美运行。它不会天真地检查address1
和address2
字面上是否具有相同的StateProvince
引用,而是实际上检查语义是否相等。另一种方法是扩展跟踪代码以实际下降到子对象。换句话说,对于每个属性,检查
Type.IsClass
和可选的Type.IsInterface
属性,如果true
,则递归调用属性本身的更改跟踪方法,以属性名称作为递归返回的任何审核结果的前缀。因此,您最终会更改StateProvinceCode
。我有时也会使用上述方法,但更容易的是,在要比较语义相等性(即审核)的对象上覆盖
Equals
并提供适当的ToString
覆盖这清楚地表明了发生了什么变化。它不能扩展深层嵌套,但我认为以这种方式进行审核是不寻常的。最后一个技巧是定义您自己的接口,例如
IAuditable
,它采用相同类型的第二个实例作为参数,并实际上返回所有差异的列表(或可枚举)。它类似于上面重写的object.Equals
方法,但返回更多信息。当对象图非常复杂并且您知道不能依赖反射或Equals
时,这非常有用。您可以将其与上述方法结合起来;实际上,您所要做的就是用IComparable
替换您的IAuditable
并调用Audit
方法(如果它实现了该接口)。IComparable
is for ordering comparisons. Either useIEquatable
instead, or just use the staticSystem.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 overridingEquals
.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:
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, butaddress1
andaddress2
still have non-null values for theStateProvince
property and each instance is different. Therefore,address1
andaddress2
have different properties.Let's flip this around, take this code as an example:
Should these be considered equal? Well, they will be, using your method, because
StateProvince
does not implementIComparable
. That's the only reason why your method reported that the two objects were the same in the original case. Since theStateProvince
class does not implementIComparable
, 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 theStateProvince
method to get better results:Once you've done this, the
object.Equals
code will work perfectly. Instead of naïvely checking whether or notaddress1
andaddress2
literally have the sameStateProvince
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 theType.IsInterface
property, and iftrue
, 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 forStateProvinceCode
.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 appropriateToString
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 overriddenobject.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 orEquals
. You can combine this with the above approach; really all you have to do is substituteIComparable
for yourIAuditable
and invoke theAudit
method if it implements that interface.这个 github 上的项目几乎可以检查任何类型的属性,并且可以根据您的需要进行自定义。
This project on github checks nearly any type of property and can be customized as you need.
您可能想看看 Microsoft 的 Testapi 它有一个可以进行深度比较的对象比较 api。这对你来说可能有点过分了,但值得一看。
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.
这里有一个简短的 LINQ 版本,它扩展了 object 并返回不相等的属性列表:
usage:
object.DetailedCompare(objectToCompare);
Here a short LINQ version that extends object and returns a list of properties that are not equal:
usage:
object.DetailedCompare(objectToCompare);
您永远不想在可变属性(可以被某人更改的属性)上实现 GetHashCode - 即非私有设置器。
想象一下这样的场景:
GetHashCode()
或直接使用(Hashtable)。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:
GetHashCode()
"under the covers" or directly (Hashtable).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.Liviu Trifoi 解决方案: 使用 CompareNETObjects 库。
GitHub - NuGet 包 - 教程。
Liviu Trifoi solution: Using CompareNETObjects library.
GitHub - NuGet package - Tutorial.
我认为这个方法非常简洁,它避免了重复或向类添加任何内容。您还在寻找什么?
唯一的选择是为新旧对象生成一个状态字典,并为它们编写比较。用于生成状态字典的代码可以重用您在数据库中存储此数据所拥有的任何序列化。
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.