Поиск различий свойств между двумя объектами 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.

8 Solutions collect form web for “Поиск различий свойств между двумя объектами C #”

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

SFINAE работает в обратном типе, но не как параметр шаблона

Android 3.1 USB-хост – BroadcastReceiver не получает USB_DEVICE_ATTACHED

Банк-кард-ридер заставляет Outlook запрашивать карту

ModelState.IsValid == false, почему?

Как настроить Outlook для отправки автоматического ответа в течение определенного часа дня каждый день?

Отключить кнопку извлечения компакт-диска на ноутбуке Windows

Мой компьютер не включится после установки нового барана

Корневая причина высокой загрузки процессора; Какое измерение больше доверять: диспетчер задач Windows или Process Explorer?

Есть ли уникальный идентификатор устройства Android?

sed не дает мне правильную операцию замены для новой строки с Mac – различия между GNU sed и BSD / OSX sed

Вычислить подобие косинуса с учетом 2 строк предложения

IDEA: javac: исходный релиз 1.7 требует целевого релиза 1.7

VNC Viewer с именем пользователя?

Как запустить программу C на Mac OS X с помощью терминала?

Имеет ли смысл устанавливать антивирус в режиме Windows XP?

Давайте будем гением компьютера.