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.
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.
Ce projet sur codeplex vérifie presque n'importe quel type de propriété et peut être personnalisé selon vos besoins.
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);
}
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; }
}
}
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:
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.
solution Liviu Trifoi: Utilisation de la bibliothèque CompareNETObjects. GitHub - package NuGet - Tutoriel .
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.
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; }
}