web-dev-qa-db-fra.com

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

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

J'ai implémenté la méthode suivante en utilisant la réflexion pour trouver des changements aux propriétés de 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 recherche System.IComparable car "Tous les types numériques (tels que Int32 et Double) implémentent IComparable, tout comme String, Char et DateTime". Cela semblait être le meilleur moyen de trouver une propriété qui n'est pas une classe personnalisée.

Taper dans l'événement PropertyChanged généré par le WCF ou le code proxy du service Web semblait bon mais ne me donne pas assez d'informations pour mes journaux d'audit (anciennes et nouvelles valeurs).

Vous cherchez à savoir s'il existe une meilleure façon de le faire, merci!

@Aaronaught, voici un exemple de code qui génère une correspondance positive basée sur la réalisation d'un objet.

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

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

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

"[Adresse] StateProvince est passé de 'MyAccountService.StateProvince' à 'MyAccountService.StateProvince'"

Il s'agit de deux instances différentes de la classe StateProvince, mais les valeurs des propriétés sont les mêmes (toutes nulles dans ce cas). Nous ne remplaçons pas la méthode des égaux.

56
Pete Nelson

IComparable sert à ordonner les comparaisons. Utilisez à la place IEquatable ou utilisez simplement la méthode statique System.Object.Equals. Ce dernier a l'avantage de fonctionner également si l'objet n'est pas de type primitif mais définit toujours sa propre comparaison d'égalité en redéfinissant 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, vous pouvez vous assurer que cela fonctionne toujours pour vos besoins particuliers.

Il existe d'autres méthodes pour comparer des objets (comme les sommes de contrôle, la sérialisation, etc.), mais c'est probablement la 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 aboutit à un "hit" est parce que les instances ne sont en fait 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 chaque instance est différente . Par conséquent, address1 Et address2 Ont des propriétés différentes.

Tournons 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, car StateProvince n'implémente pas IComparable. C'est la seule raison pour laquelle votre méthode a indiqué que les deux objets étaient les mêmes dans le cas d'origine. Étant donné que la classe StateProvince n'implémente pas IComparable, le tracker ignore simplement 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 en fait l'égalité sémantique.


L'autre solution consiste à étendre le code de suivi pour descendre réellement 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, puis appelez récursivement la méthode de suivi des modifications sur la propriété elle-même, en préfixant tous les résultats d'audit retournés récursivement avec le nom de la propriété. Vous vous retrouvez donc avec un changement pour StateProvinceCode.

J'utilise parfois l'approche ci-dessus aussi, mais il est plus facile de simplement remplacer Equals sur les objets pour lesquels vous souhaitez comparer l'égalité sémantique (c.-à-d. Audit) et de fournir une substitution appropriée de ToString qui le rend clair Qu'est ce qui a changé. Il ne s'adapte pas à l'imbrication profonde, mais je pense qu'il est inhabituel de vouloir effectuer un audit 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 comme paramètre et retourne en fait une liste (ou énumérable) de toutes les différences. Elle est similaire à notre méthode object.Equals Remplacée ci-dessus mais donne plus d'informations. Ceci est utile lorsque le graphe d'objet est vraiment compliqué et que vous savez que vous ne pouvez pas compter sur Reflection ou Equals. Vous pouvez combiner cela avec l'approche ci-dessus; en fait, tout ce que vous avez à faire est de remplacer IComparable par votre IAuditable et d'appeler la méthode Audit si elle implémente cette interface.

25
Aaronaught

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

18
bkaid

Vous voudrez peut-être regarder Testapi de Microsoft Il a une API de comparaison d'objets qui fait des comparaisons approfondies. Cela pourrait être exagéré pour vous, mais cela pourrait valoir le coup d'œil.

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
Mike Two

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

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

    }
3
Rick

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 placez une instance de votre objet dans une collection qui utilise GetHashCode () "sous les couvertures" ou directement (Hashtable).
  2. Ensuite, quelqu'un modifie la valeur du champ/propriété que vous avez utilisé dans votre implémentation GetHashCode ().

Devinez quoi ... votre objet est définitivement perdu dans la collection puisque la collection utilise GetHashCode () pour le trouver! Vous avez effectivement modifié la valeur du code de hachage par rapport à celle qui était initialement placée dans la collection. Probablement pas ce que vous vouliez.

2
Dave Black

solution Liviu Trifoi: Utilisation de la bibliothèque CompareNETObjects. GitHub - package NuGet - Tutoriel .

1
ali-myousefi

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. Que cherchez-vous de plus?

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 pourrait réutiliser toute sérialisation dont vous disposez pour stocker ces données dans la base de données.

0
Phil H

La façon de compiler mon arbre de Expression. 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
IlPADlI