Encontrar diferencias de propiedad entre dos objetos C #

El proyecto en el que estoy trabajando necesita un simple registro de auditoría para cuando un usuario cambia su correo electrónico, dirección de facturación, etc. Los objetos con los que trabajamos provienen de diferentes fonts, una es un servicio WCF y la otra es un servicio web.

Implementé el siguiente método usando la reflexión para encontrar cambios en las propiedades de dos objetos diferentes. Esto genera una lista de las propiedades que tienen diferencias junto con sus valores antiguos y nuevos.

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

Estoy buscando System.Impareable porque “Todos los tipos numéricos (como Int32 y Double) implementan IComparable, al igual que String, Char y DateTime”. Esta parecía la mejor forma de encontrar cualquier propiedad que no sea una clase personalizada.

Al tocar en el evento PropertyChanged generado por el WCF o el código proxy del servicio web sonaba bien, pero no me proporciona suficiente información para mis registros de auditoría (valores antiguos y nuevos).

Buscando información sobre si hay una mejor manera de hacerlo, ¡gracias!

@Aaronaught, aquí hay un código de ejemplo que genera una coincidencia positiva basada en hacer object.Equals:

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

“[Dirección] StateProvince cambió de ‘MyAccountService.StateProvince’ a ‘MyAccountService.StateProvince'”

Son dos instancias diferentes de la clase StateProvince, pero los valores de las propiedades son los mismos (todos nulos en este caso). No estamos anulando el método de igualdad.

IComparable es para ordenar comparaciones. O use IEquatable en IEquatable lugar, o simplemente use el método estático System.Object.Equals . El último tiene la ventaja de funcionar también si el objeto no es de tipo primitivo, pero aún define su propia comparación de igualdad anulando 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. } 

Obviamente, esto no es perfecto, pero si solo lo haces con las clases que controlas, puedes asegurarte de que siempre funcione para tus necesidades particulares.

Existen otros métodos para comparar objetos (como sums de verificación, serialización, etc.) pero es probablemente el más confiable si las clases no implementan IPropertyChanged consistente y desea conocer realmente las diferencias.


Actualización para un nuevo código de ejemplo:

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

La razón por la que usar object.Equals en su método de auditoría resulta en un “hit” es porque las instancias en realidad no son iguales.

Claro, StateProvince puede estar vacío en ambos casos, pero address1 y address2 todavía tienen valores no nulos para la propiedad StateProvince y cada instancia es diferente. Por lo tanto, address1 y address2 tienen diferentes propiedades.

Vamos a voltear esto, tome este código como un ejemplo:

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

¿Deberían ser considerados iguales? Bueno, lo harán, utilizando su método, porque StateProvince no implementa IComparable . Esa es la única razón por la cual su método informó que los dos objetos eran iguales en el caso original. Como la clase StateProvince no implementa IComparable , el rastreador simplemente omite esa propiedad por completo. ¡Pero estas dos direcciones claramente no son iguales!

Es por eso que originalmente sugerí usar object.Equals , porque entonces puedes anularlo en el método StateProvince para obtener mejores resultados:

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

Una vez que haya hecho esto, el código object.Equals funcionará perfectamente. En lugar de verificar ingenuamente si la address1 y la address2 tienen literalmente la misma referencia de la StateProvince , realmente verificará la igualdad semántica.


A la inversa, ampliar el código de seguimiento para que descienda en subobjetos. En otras palabras, para cada propiedad, compruebe Type.IsClass y, opcionalmente, la propiedad Type.IsInterface , y si es true , invoque recursivamente el método de seguimiento de cambios en la propiedad misma, anteponiendo los resultados de auditoría devueltos recursivamente con el nombre de la propiedad. Entonces terminarías con un cambio para StateProvinceCode .

A veces también uso el enfoque anterior, pero es más fácil anular Equals en los objetos para los que desea comparar la igualdad semántica (es decir, auditoría) y proporcionar una anulación de ToString adecuada que deja en claro lo que cambió. No escala para anidamiento profundo, pero creo que es inusual querer auditar de esa manera.

El último truco es definir su propia interfaz, digamos IAuditable , que toma una segunda instancia del mismo tipo como un parámetro y realmente devuelve una lista (o enumerable) de todas las diferencias. Es similar a nuestro objeto reemplazado. object.Equals método de object.Equals anterior pero ofrece más información. Esto es útil para cuando el gráfico de objetos es realmente complicado y usted sabe que no puede confiar en Reflection o Equals . Puede combinar esto con el enfoque anterior; realmente todo lo que tiene que hacer es sustituir IComparable por su IAuditable e invocar el método de Audit si implementa esa interfaz.

Este proyecto en codeplex verifica casi cualquier tipo de propiedad y se puede personalizar según lo necesite.

Es posible que desee ver el Testapi de Microsoft. Tiene una API de comparación de objetos que hace comparaciones profundas. Puede ser exagerado para ti, pero podría valer la pena verlo.

 var comparer = new ObjectComparer(new PublicPropertyObjectGraphFactory()); IEnumerable 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); } 

Nunca desea implementar GetHashCode en propiedades mutables (propiedades que alguien podría cambiar), es decir, entidades no privadas.

Imagina este escenario:

  1. usted pone una instancia de su objeto en una colección que usa GetHashCode () “debajo de las cubiertas” o directamente (Hashtable).
  2. Luego alguien cambia el valor del campo / propiedad que ha utilizado en su implementación de GetHashCode ().

Adivina qué … tu objeto se pierde de forma permanente en la colección, ya que la colección usa GetHashCode () para encontrarlo. Has cambiado efectivamente el valor de hashcode de lo que originalmente se colocó en la colección. Probablemente no sea lo que querías.

Aquí una versión corta de LINQ que extiende el objeto y devuelve una lista de propiedades que no son iguales:

uso: object.DetailedCompare (objectToCompare);

 public static class ObjectExtensions { public static List DetailedCompare(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; } } } 

Solución Liviu Trifoi: utilizando la biblioteca CompareNETObjects. GitHub – Paquete NuGet – Tutorial .

Creo que este método es bastante limpio, evita la repetición o agrega algo a las clases. Que mas estas buscando?

La única alternativa sería generar un diccionario de estado para los objetos antiguos y nuevos, y escribir una comparación para ellos. El código para generar el diccionario de estado podría reutilizar cualquier serialización que tenga para almacenar estos datos en la base de datos.

La forma de mi árbol de comstackción de mi versión. Debería ser más rápido que PropertyInfo.GetValue .

 static class ObjDiffCollector { private delegate DiffEntry DiffDelegate(T x, T y); private static readonly IReadOnlyDictionary DicDiffDels; private static PropertyInfo PropertyOf(Expression> 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(); 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(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(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; } }