2010-03-05 10 views
45

El proyecto en el que estoy trabajando necesita un registro de auditoría simple para cuando un usuario cambia su correo electrónico, dirección de facturación, etc. Los objetos con los que trabajamos provienen de diferentes fuentes, una WCF service, el otro es un servicio web.Encontrar diferencias de propiedad entre dos objetos C#

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

Busco System.IComparable porque "Todos los tipos numéricos (como Int32 y doble) implementan IComparable, al igual que las cuerdas, 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 da 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í es un código de ejemplo que está generando un resultado positivo basado en Object.equals haciendo:

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 cambiado 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.

Respuesta

22

IComparable es para hacer comparaciones de pedidos. O bien use IEquatable en su lugar, o simplemente use el método estático System.Object.Equals. Este último tiene la ventaja de que también funciona si el objeto no es un 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. 
} 

Esto, obviamente, no es perfecto, pero si sólo lo está haciendo con las clases que usted controla, entonces usted puede asegurarse de que siempre funciona para sus necesidades particulares.

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


Actualización para 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 el uso de object.Equals en los resultados del método de auditoría en un "éxito" se debe a que los casos no son realmente iguales!

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

Vamos a voltear esto alrededor, tomar 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"); 

caso de que estos se consideran 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!

Esto es por lo que se propuso inicialmente usando object.Equals, porque entonces se puede 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 hecho esto, el código object.Equals funcione a la perfección. En lugar de verificar ingenuamente si address1 y address2 tienen literalmente la misma referencia StateProvince, en realidad comprobará la igualdad semántica.


La otra forma de evitar esto es extender el código de seguimiento para realmente descender a los subobjetos. En otras palabras, para cada propiedad, marque la 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, prefijando cualquier resultado de auditoría devuelto recursivamente con el nombre de la propiedad. Entonces terminarías con un cambio por StateProvinceCode.

uso el enfoque anterior a veces demasiado, pero es más fácil simplemente anular Equals en los objetos para los que desea comparar la igualdad semántica (es decir, de auditoría) y proporcionar un reemplazo adecuado ToString que deja claro lo que ha cambiado. 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<T>, que toma una segunda instancia del mismo tipo como parámetro y realmente devuelve una lista (o enumerable) de todas las diferencias. Es similar a nuestro método object.Equals reemplazado 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 Audit si implementa esa interfaz.

+0

usando Desafortunadamente solo Object.equals está volviendo cierto para los tipos de referencia, por ejemplo: [Dirección] StateProvince cambió de 'MyAccountService.StateProvince' a 'MyAccountService.StateProvince' –

+0

@Pete Nelson: Suponiendo que estés en realidad, comparando diferentes referencias, eso es ... improbable. ¿Podemos ver un ejemplo completo con la clase? ¿Reemplaza el método 'Igual'? Uso un código muy similar a esto y nunca da falsos negativos. – Aaronaught

+0

Se agregó un ejemplo de coincidencia con la publicación original. –

9

Es posible que desee ver Microsoft's Testapi 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<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); 
} 
0

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.

18

This proyecto en codeplex comprueba casi cualquier tipo de propiedad y se puede personalizar según sea necesario.

+0

De algunas pruebas superficiales, esto también se ve bastante bien. Por lo menos, su código fuente me brinda más información sobre cómo están haciendo su comparación de objetos. –

+0

gracias por el enlace thekaido, me has ahorrado mucho tiempo implementando una biblioteca similar yo mismo :) –

+0

Aquí hay otra que usa árboles de expresiones. Probablemente más rápido. https://github.com/StevenGilligan/AutoCompare –

2

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

Imagínese este escenario:

  1. pones una instancia de su objeto en una colección que utiliza GetHashCode() "bajo las sábanas" o directamente (Hashtable).
  2. Entonces 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.

2

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

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

    } 
+0

Si bien este código puede responder a la pregunta, proporcionar un contexto adicional con respecto a cómo y/o por qué resuelve el problema mejoraría el valor de la respuesta a largo plazo. Lea este [cómo-para-responder] (http://stackoverflow.com/help/how-to-answer) para proporcionar una respuesta de calidad. – thewaywewere

+0

Esto solo funciona para tipos de propiedades base. Las listas de objetos secundarios siempre se informarán como diferentes, incluso si son iguales. –

0

El camino de Expression árbol compile la versión. Debería ser más rápido que 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; } 
} 
Cuestiones relacionadas