Поиск различий свойств между двумя объектами C #

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

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

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

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

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

@Aaronaught, вот пример кода, который генерирует положительное соответствие, основанное на выполнении объекта. 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 »»

Это два разных экземпляра classа StateProvince, но значения свойств одинаковы (в этом случае все нули). Мы не переопределяем метод equals.

IComparable предназначен для сравнения. IEquatable этого используйте 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. } 

Это, очевидно, не идеально, но если вы делаете это только с classами, которыми вы управляете, вы можете убедиться, что он всегда работает для ваших конкретных потребностей.

Существуют и другие методы для сравнения объектов (например, контрольные суммы, сериализация и т. Д.), Но это, вероятно, самый надежный, если classы не реализуют 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 . Это единственная причина, по которой ваш метод сообщил, что оба объекта были одинаковыми в исходном случае. Поскольку class 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 код object.Equals будет работать отлично. Вместо того, чтобы наивно проверять, имеет ли address1 и address2 буквально одну и StateProvince же ссылку StateProvince , он фактически проверяет семантическое равенство.


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

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

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

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

Возможно, вам захочется взглянуть на тестовое приложение Microsoft. У него есть сравнение объектов api, которое делает глубокие сравнения. Это может быть излишним для вас, но это может стоить взгляда.

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

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

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

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

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

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

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

Решение Liviu Trifoi: Использование библиотеки CompareNETObjects. GitHub – пакет NuGet – учебник .

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

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

Мой путь к дереву Expression . Он должен быть быстрее, чем 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; } } 
Давайте будем гением компьютера.