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

проект, над которым я работаю, требует простого ведения журнала аудита, когда пользователь меняет свою электронную почту, адрес выставления счетов и т. д. Объекты, с которыми мы работаем, поступают из разных источников: один-служба 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;
}

Я ищу систему.IComparable, потому что " все числовые типы (такие как Int32 и Double) реализуют IComparable, как String, Char и DateTime."Это казалось лучшим способом найти любое свойство, которое не является пользовательским классом.

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

Поиск ввода, если есть лучший способ сделать это, спасибо!

@Aaronaught, вот пример кода, который генерирует положительное совпадение на основе объекта doing.Равно:

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

8 ответов


IComparable заказ для сравнения. Либо используйте IEquatable вместо этого или просто используйте static 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>, который принимает второй экземпляр того же типа в качестве параметра и фактически возвращает список (или перечисляемые) всех различий. Это похоже на наш overridden object.Equals метод выше, но дает больше информации. Это полезно, когда график объектов действительно сложный, и вы знаете, что не можете полагаться на отражение или Equals. Вы можете объединить это с вышеуказанным подходом; на самом деле все, что вам нужно сделать, это заменить IComparable для IAuditable и вызвать Audit метод, если он реализует этот интерфейс.


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


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

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

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

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

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


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

использование: объект.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; }
        }

    }

решение Ливиу Трифои: использование библиотеки CompareNETObjects. GitHub -пакета NuGet -учебник.


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

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


мой путь 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; }
}