2010-03-05 4 views
45

Проект, над которым я работаю, требует некоторого простого аудита, когда пользователь меняет свой адрес электронной почты, адрес выставления счетов и т. Д. Объекты, с которыми мы работаем, исходят из разных источников, WCF, другой - веб-сервис.Поиск различий свойств между двумя объектами C#

Я использовал следующий метод, используя отражение, чтобы найти изменения свойств на двух разных объектах. Это создает список свойств, которые имеют отличия, а также их старые и новые значения.

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." Это казалось лучшим способом найти любое свойство, которое не является обычным классом.

Нажатие на событие PropertyChanged, созданное с помощью прокси-кода WCF или веб-службы, звучит неплохо, но не дает мне достаточно информации для моих журналов аудита (старые и новые значения).

Ищите информацию о том, есть ли лучший способ сделать это, спасибо!

@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, но значения свойств одинаковы (в этом случае все нули). Мы не переопределяем метод equals.

ответ

22

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 может быть пустым в обоих случаях, но address1 и address2 до сих пор не нулевые значения для StateProvince собственности и каждый экземпляр отличается. Поэтому address1 и address2 имеют разные свойства.

Давайте флип это вокруг, возьмите этот код в качестве примера:

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 код будет работать отлично. Вместо того, чтобы наивно проверять, действительно ли address1 и address2 имеют то же самое значение StateProvince, он фактически проверяет семантическое равенство.


Другой способ заключается в расширении кода отслеживания для фактического спуска в под-объекты. Другими словами, для каждого свойства проверьте Type.IsClass и необязательно свойство Type.IsInterface, а если true, то рекурсивно вызовите метод отслеживания изменений для самого свойства, префикс любых результатов аудита, возвращаемых рекурсивно с именем свойства. Таким образом, вы закончите с изменением для StateProvinceCode.

Я использую выше подход иногда слишком, но это проще просто переопределить Equals на объекты, для которых вы хотите сравнить семантическое равенство (т.е. аудита) и обеспечить соответствующее ToString переопределение, что дает понять, что изменилось. Он не масштабируется для глубокого гнездования, но я думаю, что это необычно, чтобы это было так.

Последний трюк заключается в том, чтобы определить свой собственный интерфейс, скажем IAuditable<T>, который принимает второй экземпляр того же типа, что и параметр, и фактически возвращает список (или перечислимый) всех различий. Это похоже на наш переопределенный метод object.Equals выше, но возвращает дополнительную информацию. Это полезно, когда граф объектов действительно сложный, и вы знаете, что не можете полагаться на Reflection или Equals. Вы можете комбинировать это с вышеуказанным подходом; на самом деле все, что вам нужно сделать, это заменить IComparable на ваш IAuditable и вызвать метод Audit, если он реализует этот интерфейс.

+0

К сожалению, используя только Object.equals возвращается справедливо для ссылочных типов, например: [Адрес] StateProvince изменен с «MyAccountService.StateProvince» до «» MyAccountService.StateProvince –

+0

@Pete Нельсон: Предполагая, что вы фактически сравнивая разные ссылки, это ... невероятно. Можем ли мы увидеть полный пример с классом? Переопределяет ли это метод 'Equals'? Я использую код, очень похожий на этот, и он никогда не дает ложных негативов. – Aaronaught

+0

Добавлен пример матча к исходному сообщению. –

9

Возможно, вы захотите посмотреть Microsoft's 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); 
} 
0

Я думаю, что этот метод довольно опрятный, он избегает повторения или добавления чего-либо к классам. Что еще вы ищете?

Единственной альтернативой может быть создание государственного словаря для старого и нового объектов и запись для них сравнения. Код для создания словаря состояния может повторно использовать любую сериализацию, которую вы имеете для хранения этих данных в базе данных.

18

This Проект на codeplex проверяет почти любой тип имущества и может быть настроен по мере необходимости.

+0

От некоторых беглых испытаний это тоже выглядит довольно хорошо. По крайней мере, их исходный код дает мне больше информации о том, как они проводят сравнение объектов. –

+0

спасибо за ссылку thekaido, вы спасли меня много времени на реализацию подобной библиотеки :) –

+0

Вот еще один, который использует деревья выражений. Наверное, быстрее. https://github.com/StevenGilligan/AutoCompare –

2

Вы никогда не хотите внедрять GetHashCode в изменяемые свойства (свойства, которые могут быть изменены кем-то) - то есть не частные установки.

Представьте себе такой сценарий:

  1. вы положили экземпляр вашего объекта в коллекции, которая использует GetHashCode() «под одеялом», или непосредственно (Hashtable).
  2. Затем кто-то изменяет значение поля/свойства, которое вы использовали в реализации GetHashCode().

Угадайте, что ... ваш объект навсегда потерян в коллекции, так как коллекция использует GetHashCode(), чтобы найти его! Вы эффективно изменили значение hashcode из того, что изначально было размещено в коллекции. Наверное, не то, что вы хотели.

2

Вот краткая версия LINQ, который расширяет объект и возвращает список свойств, которые не равны:

использование: 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

Хотя этот код может ответить на вопрос, предоставление дополнительного контекста относительно того, как и/или почему оно решает проблему, улучшит долгосрочную ценность ответа. Пожалуйста, прочтите это [как-ответ] (http://stackoverflow.com/help/how-to-answer) для обеспечения качественного ответа. – thewaywewere

+0

Это работает только для базовых типов свойств. Списки дочерних объектов всегда будут отображаться как разные, даже если они одинаковы. –

0

Мой способ 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; } 
}