Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Fathi Bellahcene

.Net m'a tuer!

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:

  1. 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
  2. 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 clip_image001

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:

image_thumb1

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:

image_thumb3

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 :
Posted: mardi 3 août 2010 22:56 par fathi
Classé sous : , , , ,

Commentaires

abolibibelot a dit :

Bonjour Fathi (ça va ? Clearcase te manque pas trop ?),

J'aime beaucoup ta série de présentations sur PostSharp, un outil à mon avis trop méconnu, et ton exemple est très intéressant.

Cela dit ton histoire de classe de base me chiffonne, ça oblige à polluer des classes métier avec de la plomberie, et ça grille un joker (héritage simple).

Puisque tu fais du Postsharp, pourquoi ne pas se lâcher et ajouter un mixin dans l'histoire, en introduisant lors du tissage une interface sur ta classe à auditer - IAuditable par exemple - et en en profitant pour fournir une implémentation (cf. http://doc.sharpcrafters.com/postsharp/2.0/##PostSharp.chm/html/326746a1-0d0f-4e84-872e-ced2b2a9d500.htm ).

Tout fonctionne comme ta classe de base, mais tu gardes tes POCOs et tu sélectionnes ce que tu veux auditer via un attribut multicast et la vie est belle et sans héritage.

Ca peut se faire par des proxies dynamiques aussi mais c'est plus galère.

Yann.

# août 4, 2010 00:39

Thomas LEBRUN a dit :

Hello Fathi,

Sympa les posts sur PostSharp.

Sinon, une remarque: pourquoi ne pas avoir utiliser IEditableObject sur ton objet (plutot que passer par une classe de base) ?

A+

# août 4, 2010 06:52

fathi a dit :

@abolibibelot

Hello Yann! Moi ça va très bien et j'espère que toi aussi!

Non ClearCase ne me manque pas...vu que je l'utilise actuellement :)

@Thomas

Salut et merci! content de te voir ici!

@vous deux:

Vous avez tout a fait raison! et PostSharp permet de faire cela très simplement avec l'attribut CompositionAspect:

Cet attribut va permettre d'injecter le code associée à l'interface (IAuditable ou IEditableObject)...et là ça devient carrément du SMP (Syvain Mirouf Programming)!

Pour illustrer ca, l'article de Codingly est génial et je pense pas pouvoir faire mieux (et j'ai pas envie de faire un blog copy/paste)...Mais pour faire plaisir à Yann, je vais faire une version avec cet attribut (après tout t'a aussi bossé sur SFY)

Pour être tout à fait franc avec vous, le code produit chez mon client devait répondre à une problématique un peu plus complexe et la classe de base existait déjà.

En +, l'idée dans ce post était de présenter le nouvel attribut "LocationInterceptionAspect" sur un exemple le plus simple possible mais qui répond à une véritable problématique: d'ailleurs, si vous regardez bien le code il est volontairement simplifié a outrance pour être sûr que tout le monde le comprenne (je règle la compréhension sur mon niveau).

# août 4, 2010 10:26

fabrice.michellonet a dit :

Hello Fathi,

tout d'abord je suis content de voir que tu te tiens au rythme de publication que tu t'étais plus ou moins imposé.

Je me joins aux autres pour te féliciter pour cette suite d'article qui sort un peu des sentiers battus; le sujet est intéressant.

Concernant la mise en forme, les blocs de code sont bien plus lisible dans ce deuxième post, merci pour nos yeux :)

Maintenant sur le fond :

Si j'avais rencontré la même situation que celle que tu décris sur un projet en prod je pense que j'aurais penché sur une solution à base de Dynamic Proxy (celui de castle surement) et comme le proposait Yann avec introduction d'un mixin  par l'implémentation d'une interface.

(cf : http://kozmic.pl/archive/2009/08/12/castle-dynamic-proxy-tutorial-part-xiii-mix-in-this-mix.aspx).

J'en arrive donc à ma question ouverte à vous tous; pourquoi dans ce cas précis préférer une solution a base d'injection d'IL directement dans les objets métiers plutôt que d'utiliser des proxy?

[Évidement l'injection directe est plus performante, mais doit surement rendre le debug plus compliqué, et la maintenabilité en souffre, non?].

# août 5, 2010 09:14
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01