Faire de l’audit avec PostSharp
Lors de mon précédent post, j’ai parlé de PostSharp et des différentes utilisations possibles de ce Framework d’AOP (Programmation orienté Aspect). Ici je vais vous présenter un nouveau type d’attribut qui arrive avec la V2 de PostSharp l’attribut LocationInterceptionAspect
Son principal intérêt est qu’il fonctionne aussi bien sur les fields que sur les propriétés (accesseurs), et ca c’est vachement cool!
Un cas concret
Sur un projet en winform, l’équipe technique me posse le cas suivant: “on a des objets métiers que l’on souhaite binder directement avec les écrans, On a deux problèmes:
- sur tous les écrans on a un bouton cancel…et on veut pas avoir à stocker une copie de l’objet initial quelque part ou recharger l’objet
- On souhaite connaitre la liste des propriétés qui ont été modifiés ainsi que les anciennes valeurs (pour faire de l’audit)
et bien sûr cela avec le moins d’impact possible sur leur code!
Ma solution
Après avoir réfléchit un peu (oui, ça m’arrive de temps en temps):
Je me dit que les objets métiers doivent tous hériter d’une classe de base qui contiendra un dictionnaire avec en clé le nom de la propriété et en valeur la valeur initiale de l’objet.
Une méthode CancelChanges va se charger de restaurer (par réflexion) les valeurs initiales de l’objet.
Un booléen EnableAudit va permettre d’activer/désactiver cette comparaison: ainsi, la “feature” est désactivé lorsque l’on charge l’objet (depuis une factory par exemple).
public abstract class BaseClass
{
private Dictionary<string, object> oldProperties = new Dictionary<string,object>();
public Dictionary<string,object> OldProperties
{
get { return oldProperties; }
set { oldProperties = value;}
}
public bool EnableAudit {get;set;}
public void CancelChanges()
{
foreach (KeyValuePair<string,object> item in OldProperties.ToList())
{
PropertyInfo p = this.GetType().GetProperty(item.Key);
if (p != null)
p.SetValue(this, item.Value, null);
else
{
FieldInfo f = this.GetType().GetField(item.Key);
if(f!=null)
f.SetValue(this, item.Value);
}
}
oldProperties.Clear();
}
}
Dans mon exemple, vous aurez remarqué que la méthode CancelChanges se charge de restaurer aussi bien les propriétés que les fields. Normalement, un field ne doit jamais être public …mais j’ai appris avec le temps qu’il est préférable de se préparer au pire
Il nous reste plus qu’a alimenter notre dictionnaire à chaque modification d’une propriété (c’est ici que les bactéries attaque PostSharp entre en jeux)! On va comparer la nouvelle valeur de la propriété avec l’ancienne, et si elles sont différentes, on stocke la valeur dans le dictionnaire et tout ca bien sûr dans un bel attribut PostSharp!
[Serializable]
public class AuditableObject: LocationInterceptionAspect
{
public override void OnSetValue(LocationInterceptionArgs args)
{
string name = args.Location.PropertyInfo.Name;
if ( args.Instance is BaseClass)
{
BaseClass currentObject = (BaseClass)args.Instance;
if (currentObject.EnableAudit)
{
object currentValue = args.GetCurrentValue();
object newValue = args.Value;
if (currentValue != newValue)
{
if (!currentObject.OldProperties.ContainsKey(name))
currentObject.OldProperties.Add(name, currentValue);
}
}
}
base.OnSetValue(args);
}
}
Ici, on intercepte les modifications (on “set” une propriété ou un fielp)
- on va d’abord tester si notre objet hérite de notre classe de base (je n’aime pas trop ça, mais ici on a pas le choix étant donné que cet attribut peut être posé sur n’importe quel classe)
- ensuite voir si la feature est activé
- et ensuite comparer la nouvelle valeur avec l’ancienne si on ne change pas la valeur on fait rien
- et enfin dans le cas ou il n’existe pas déjà une entrée dans le dictionnaire (si c’est la deuxième modification par exemple), on ajoute l’ancienne avec le nom de la propriété ou du field qui va bien!
On remarque aussi que l’on hérite de la classe LocationInterceptionAspect…on va donc pouvoir intercepter des modifications sur les propriétés et sur les fields.
Exemple d’utilisation:
[AuditableObject]
public class User:BaseClass
{
[AuditableObject(AttributeExclude=true)]
private int id;
public int Id
{
get { return id; }
set { id = value; }
}
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string Code = "Init";
}
Dans cet exemple, on a positionné l’attribut sur la classe et donc cet attribut va s’exécuter sur l’ensemble.
Un petit test…
class Program
{
static void Main(string[] args)
{
User myUser = new User();
//Initialisation Step
myUser.Id = 4;
myUser.Name = "Befa";
myUser.BirthDate = new DateTime(1979, 05, 05);
//active audit
myUser.EnableAudit = true;
//change data
myUser.BirthDate = DateTime.Today;
myUser.Id += 1;
myUser.Id += 1;
myUser.Name = "BEFA";
myUser.Code = "sSources";
//
Console.WriteLine("il y a {0} propriétés qui ont été modifiées:", myUser.OldProperties.Count);
myUser.OldProperties.ToList().ForEach( item => Console.WriteLine( " ---> la propriété {0} a la valeur initiale: {1} ", item.Key,item.Value.ToString()));
Console.WriteLine("Valeurs courantes:");
PrintCurrentInfo(myUser);
myUser.CancelChanges();
Console.WriteLine("Valeurs après Cancel:");
PrintCurrentInfo(myUser);
Console.ReadKey();
}
private static void PrintCurrentInfo(User myUser)
{
Console.WriteLine(string.Empty);
Console.WriteLine(string.Empty);
Console.WriteLine("---> Id :" + myUser.Id);
Console.WriteLine("---> BirthDate :" + myUser.BirthDate);
Console.WriteLine("---> Code :" + myUser.Code);
}
}
Le scénario est le suivant:
- on définit un user
- on active l’audit
- on modifies les propriétés
- on controle deux choses
- on affiche à la console les valeurs stockées dans le dictionnaire pour vérifier que les données ont bien été sauvegardées
- on affiche les informations contenues dans l’objet avant et après avoir exécuter la méthode “CancelChanges”
après exécution, on obtient le résultat suivant à la console:

On va commencer par les bonnes nouvelles: Le cancel fonctionne bien!
puis la “mauvaise” nouvelle: lorsque l’on analyse le contenu du dictionnaire, il y a deux valeurs un peu “étranges”:
- k__BackingField
- k__BackingField
Ces champs correspondent aux fields créer par le framework .Net, lorsque l’on déclare une propriété de la manière suivante:
public string Name { get; set; }
Le framework ajoute les “backingField associé…et PostSharp ajoute le code liée à l’attribut à ces field.
Donc, on a intercepté un évènement “OnSet” sur la propriété “Name” mais aussi sur le field “k__BackingField” d’ou les deux valeurs dans le dictionnaire.
Si on regarde les informations liées à la propriété Id et le field id, on a pas le problème car le backing field n’existe pas et que l’on a spécifié que le field id doit être ignoré par notre attribut. Pour moi, cela constitue LA solution propre pour éviter d’ajouter du code IL sur le field mais ca implique de coder complètement les propriétés.
Une autre méthode est de détecter dans l’attribut si on réagit à un field auto généré : pour cela, il suffit de tester le nom du champ, si il commence par
[Serializable]
public class AuditableObject : LocationInterceptionAspect
{
public override void OnSetValue(LocationInterceptionArgs args)
{
string name = args.Location.PropertyInfo.Name;
//prevent backing field
if (!name.StartsWith(") && args.Instance is BaseClass)
{
BaseClass currentObject = (BaseClass)args.Instance;
if (currentObject.EnableAudit)
{
object currentValue = args.GetCurrentValue();
object newValue = args.Value;
if (currentValue != newValue)
{
if (!currentObject.OldProperties.ContainsKey(name))
currentObject.OldProperties.Add(name, currentValue);
}
}
}
base.OnSetValue(args);
}
}
Ce qui nous donne pour le même test le résultat suivant:

Ce qui est plus conforme avec ce que l’on attend!
Conclusion
Ici on a vu que l’utilisation de l’AOP nous permet de résoudre des problèmes complexes en très peu de ligne de code et de façon générique. On également a pu constater qu’avec PostSharp que très souvent on est confronté à des problèmes plus technique liée à l’injection du code IL mais cela ne doit pas vous décourager à utiliser cet outil (qui je le rappelle est gratuit dans sa version 1.5)
Un autre nouveauté assez sexy dans la V2 est la possibilité d’associer des attributs à des interfaces…si j’ai un peu de temps, j’en parlerai dans un autre post.
Il y a aussi un excellent Post au sujet de PostSharp que je vous invite a lire et qui est très intéressant pour les projets WCF ou Silverlight: Il vous montre comment utiliser PostSharp pour implémenter INotifyPropertyChanged.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :