Trouver des différences de propriété entre deux objets C#

Le projet sur lequel je travaille nécessite une simple journalisation d'audit lorsqu'un utilisateur change son adresse e-mail, son adresse de facturation, etc. Les objets avec lesquels nous travaillons proviennent de différentes sources, l'une d'un service WCF, l'autre d'un service web.

J'ai implémenté la méthode suivante en utilisant la réflexion pour trouver les modifications apportées aux propriétés sur deux objets différents. Cela génère une liste des propriétés qui ont des différences avec leurs anciennes et nouvelles valeurs.

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

Je suis à la recherche d' Système.IComparable car " tous les types numériques (tels que Int32 et Double) implémentent IComparable, tout comme String, Char et DateTime."Cela semblait la meilleure façon de trouver une propriété qui n'est pas une classe personnalisée.

Exploiter L'événement PropertyChanged généré par le code proxy WCF ou web service semblait bien, mais ne me donne pas assez d'informations pour mes journaux d'audit (anciennes et nouvelles valeurs).

Vous cherchez une entrée pour savoir s'il y a une meilleure façon de le faire, merci!

@ Aaronaught, voici un exemple de code qui génère une correspondance positive basée sur doing object.Égal à:

Address address1 = new Address();
address1.StateProvince = new StateProvince();

Address address2 = new Address();
address2.StateProvince = new StateProvince();

IList list = Utility.GenerateAuditLogMessages(address1, address2);

" [adresse] StateProvince changé de "MyAccountService.StateProvince' à "MyAccountService.StateProvince" "

Ce sont deux instances différentes de la classe StateProvince, mais les valeurs des propriétés sont les mêmes (toutes null dans ce cas). Nous ne dépassons pas la méthode equals.

50
demandé sur Pete Nelson 2010-03-05 18:46:21

8 réponses

IComparable est pour commander des comparaisons. Utilisez IEquatable à la place, ou utilisez simplement la méthode statique System.Object.Equals. Ce dernier a l'avantage de fonctionner également si l'objet n'est pas un type primitif mais définit toujours sa propre comparaison d'égalité en remplaçant 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.
}

Ce n'est évidemment pas parfait, mais si vous ne le faites qu'avec des classes que vous contrôlez, alors vous pouvez vous assurer que cela fonctionne toujours pour vos besoins particuliers.

Il existe d'autres méthodes pour comparer des objets (tels que sommes de contrôle, sérialisation, etc.) mais c'est probablement le plus fiable si les classes n'implémentent pas systématiquement IPropertyChanged et que vous voulez réellement connaître les différences.


Mise à jour pour un nouvel exemple de code:

Address address1 = new Address();
address1.StateProvince = new StateProvince();

Address address2 = new Address();
address2.StateProvince = new StateProvince();

IList list = Utility.GenerateAuditLogMessages(address1, address2);

La raison pour laquelle l'utilisation de object.Equals dans votre méthode d'audit entraîne un " hit " est que les instances ne sont pas égales!

Bien sûr, le StateProvince peut être vide dans les deux cas, mais address1 et address2 ont toujours des valeurs non nulles pour la propriété StateProvince et chacun instance est différente. Par conséquent, address1 et address2 ont des propriétés différentes.

Retournons ceci, prenons ce code comme exemple:

Address address1 = new Address("35 Elm St");
address1.StateProvince = new StateProvince("TX");

Address address2 = new Address("35 Elm St");
address2.StateProvince = new StateProvince("AZ");

Devraient-ils être considérés comme égaux? Eh bien, ils le seront, en utilisant votre méthode, parce que StateProvince n'implémente pas IComparable. C'est la seule raison pour laquelle votre méthode a signalé que les deux objets étaient les mêmes dans le cas d'origine. Puisque la classe StateProvince n'implémente pas IComparable, le tracker ignore entièrement cette propriété. Mais ces deux adresses ne sont clairement pas égales!

C'est pourquoi j'ai initialement suggéré d'utiliser object.Equals, car alors vous pouvez le remplacer dans la méthode StateProvince pour obtenir de meilleurs résultats:

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

Une fois que vous avez fait cela, le code object.Equals fonctionnera parfaitement. Au lieu de vérifier naïvement si address1 et address2 ont littéralement la même référence StateProvince, il vérifiera réellement l'égalité sémantique.


L'inverse est d'étendre le code de suivi à descendez dans les sous-objets. En d'autres termes, pour chaque propriété, vérifiez la propriété Type.IsClass et éventuellement la propriété Type.IsInterface, et si true, appelez récursivement la méthode de suivi des modifications sur la propriété elle-même, en préfixant les résultats d'audit renvoyés récursivement avec le nom de la propriété. Donc, vous finiriez avec un changement pour StateProvinceCode.

J'utilise parfois aussi l'approche ci-dessus, mais il est plus facile de remplacer simplement Equals sur les objets pour lesquels vous voulez comparer l'égalité sémantique (c'est-à-dire l'audit) et fournissez une substitution ToString appropriée qui indique clairement ce qui a changé. Il ne s'adapte pas à l'imbrication profonde, mais je pense qu'il est inhabituel de vouloir vérifier de cette façon.

La dernière astuce consiste à définir votre propre interface, disons IAuditable<T>, qui prend une deuxième instance du même type qu'un paramètre et renvoie en fait une liste (ou énumérable) de toutes les différences. C'est similaire à notre méthode surchargée object.Equals ci-dessus mais donne plus d'informations. Ceci est utile lorsque le graphe d'objet est vraiment compliqué et vous savez que vous ne pouvez pas compter sur la réflexion ou Equals. Vous pouvez combiner cela avec l'approche ci-dessus; vraiment tout ce que vous avez à faire est de remplacer IComparable pour votre IAuditable et d'appeler la méthode Audit si elle implémente cette interface.

24
répondu Aaronaught 2010-03-05 20:06:50

Ce projet sur codeplex vérifie presque n'importe quel type de propriété et peut être personnalisé selon vos besoin.

18
répondu bkaid 2010-03-05 16:14:01

Vous pouvez regarder Testapi de Microsoft il a une api de comparaison d'objets qui fait des comparaisons profondes. Cela pourrait être exagéré pour vous, mais cela pourrait valoir le coup d'oeil.

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);
}
10
répondu Mike Two 2010-03-05 16:06:32

Vous ne voulez jamais implémenter GetHashCode sur des propriétés mutables (propriétés qui pourraient être modifiées par quelqu'un)-c'est-à-dire des setters non privés.

Imaginez ce scénario:

  1. vous mettez une instance de votre objet dans une collection qui utilise GetHashCode() "sous les couvertures" ou directement (Hashtable).
  2. Ensuite, quelqu'un change la valeur du champ/propriété que vous avez utilisé dans votre implémentation GetHashCode ().

Devine quoi...votre objet est définitivement perdu dans la collection puisque la collection utilise GetHashCode () pour le trouver! Vous avez effectivement modifié la valeur de hashcode de ce qui a été initialement placé dans la collection. Probablement pas ce que tu voulais.

2
répondu Dave Black 2012-05-17 23:22:03

Voici une version courte de LINQ qui étend l'objet et renvoie une liste de propriétés qui ne sont pas égales:

Utilisation: objet.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
répondu Rick 2017-06-26 13:02:07

Solution Liviu Trifoi: en utilisant la bibliothèque CompareNETObjects. GitHub - package NuGet - Tutoriel.

1
répondu ali-myousefi 2018-01-09 13:08:31

Je pense que cette méthode est assez soignée, elle évite la répétition ou l'ajout de quoi que ce soit aux classes. Plus ce que vous recherchez?

La seule alternative serait de générer un dictionnaire d'État pour les anciens et les nouveaux objets, et d'écrire une comparaison pour eux. Le code de génération du dictionnaire d'état peut réutiliser toute sérialisation que vous avez pour stocker ces données dans la base de données.

0
répondu Phil H 2010-03-05 15:55:35

Ma façon de Expression version de compilation de l'arbre. Il devrait être plus rapide que 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
répondu IlPADlI 2018-02-06 20:39:01