Нахождение различий свойств между двумя объектами C # - PullRequest
53 голосов
/ 05 марта 2010

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

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

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, вот пример кода, который генерирует положительное совпадениеосновано на выполнении объекта.экземпляры класса StateProvince, но значения свойств одинаковы (в этом случае все имеют значение null).Мы не переопределяем метод equals.

Ответы [ 8 ]

25 голосов
/ 05 марта 2010

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

18 голосов
/ 05 марта 2010

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

10 голосов
/ 05 марта 2010

Возможно, вы захотите взглянуть на Testapi от Microsoft У него есть 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);
}
3 голосов
/ 26 июня 2017

Здесь короткая версия 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; }
        }

    }
2 голосов
/ 18 мая 2012

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

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

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

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

1 голос
/ 09 января 2018

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

0 голосов
/ 06 февраля 2018

Мой способ 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; }
}
0 голосов
/ 05 марта 2010

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

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

...