Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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, если он реализует этот интерфейс.

Ответ 2

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

Ответ 3

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

Ответ 4

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

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

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

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

Ответ 5

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

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

    }

Ответ 6

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

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