Entity Framework et AOP

Publié par Fabrice Michellonet sous le(s) label(s) , , , le 7 octobre 2011

Cela fait des mois que je repousse la publication d'un post sur l'AOP, car je ne voulais pas vous resservir le sempiternel exemple de mise en place d'une gestion de log applicatif simplifiée.
Si vous voulez vous rafraichir les idées sur le sujet je vous conseille de jeter un œil sur l'article d'Ayende Rahien sur le sujet

Avec le framework .NET, il existe au moins 6 façons différentes d'ajouter un brin d'AOP dans vos programmes;
Pour mémoire il s'agit de :
  • Remoting Proxies
  • Dériver votre classe de ContextBoundObject
  • Passer par un dynamique proxy ( ex : Castle Dynamic Proxy)
  • Utiliser l'API de profiling de .NET
  • Injection d'IL après compilation
  • Injection d'IL au runtime

Dans cet article on s'intéressera particulièrement a l'injection post compilation, tout simplement car c'est la plus performante (le code lié à l'aspect est directement inscrit dans l'assembly finale et rien ne le distingue du reste du code) et aussi car c'est la façon la plus sexy a mon gout de faire de l'AOP (c'est une raison comme une autre, non?).

Bon revenons à nos moutons.

Sur presque tous les projets sur lesquels je suis intervenu ces dernières années, lorsqu'on modélise les entités qui devront être persistées en base, on leur adjoint au moins quatre propriétés :
  • Created By (string)
  • Created (datetime)
  • Last Updated By (string)
  • Last Updated (datetime)

J'ai pour habitude de nommer cette construction, une entité "Auditable", ce qui se traduit en code par l'interface suivante :
public interface IAuditable
    {
        string CreatedBy { get; set; }
        DateTime Created { get; set; }
        string UpdatedBy { get; set; }
        DateTime? Updated { get; set; }
    }

Vous l'aurez compris, l'idée ici, est de stocker la date et l'utilisateur ayant créé ou modifié l'entité en question et ceci a chaque accès base.
Je vous laisse imaginer le travail rébarbatif que cela peut vite devenir si l'on doit tout gérer à la main et si notre modèle est composé de dizaines voire de centaines d'entités.

Je vous propose donc une idée afin de se faciliter la vie grâce à l'AOP.
Il est à noter que bien que l'exemple ci-dessous s’appuie sur Entity Framework, le mécanisme est très certainement transposable (avec adaptation) aux autres ORM.

On commence par utiliser Entity Framework Code First, et on ajoute notre framework AOP préféré
Install-Package EntityFramework
Install-Package Afterthought

En attendant qu'un de mes patch soit accepté et intégré à Afterthought, il vous faudra remplacer la dll d'Afterthought par la mienne disponible ici.

Nuget a rajouté quelques dll et références dans votre projet et modifié également le post build event de votre projet;
Désormais à chaque compilation, Afterthought scannera les assemblies à la recherche de taches d'injection d'IL à effectuer.

Voyons comment demander à Afterthought d'injecter l'interface IAuditable sur nos entités.

public class AuditableAmender<T> : Amendment<T, T>
    {

        public AuditableAmender()
        {
            Properties.Add<string>("CreatedBy");
            Properties.Add<DateTime>("Created");
            Properties.Add<string>("UpdatedBy");
            Properties.Add<DateTime?>("Updated");

            Implement<IAuditable>();
        }
    }

Le code me semble assez clair sans avoir a revenir longuement dessus; On demande simplement à Afterthought d'injecter les propriétés nécessaires à l'implémentation de l'interface IAuditable.

Bien créons un attribut de marquage, que nous placerons sur nos entités :

[AttributeUsage(AttributeTargets.Class)]
    public class AuditableAttribute : Attribute { }

il nous reste encore deux tâches à réaliser;
  • Préciser les assemblies à introspecter
  • Permettre à Afterthought de découvrir les classes qui doivent être modifiées et surtout comment.
Cela se fait dans une même classe :

[AttributeUsage(AttributeTargets.Assembly)]
    public class AmendAttribute : Attribute, IAmendmentAttribute
    {
        IEnumerable<ITypeAmendment> IAmendmentAttribute.GetAmendments(Type target)
        {
            if (target.GetCustomAttributes(typeof(AuditableAttribute), true).Length > 0)
            {
                ConstructorInfo constructorInfo = typeof(AuditableAmender<>).MakeGenericType(target).GetConstructor(Type.EmptyTypes);
                if (constructorInfo != null)
                    yield return (ITypeAmendment)constructorInfo.Invoke(new object[0]);
            }
        }
    }
En clair, pour chaque classe qui implémente IAuditable, on va faire appel à la classe AuditableAmender (créée précédemment) pour modifier la classe.
En appliquant cet attribut sur l'assembly qui contient vos entités, Afterthought effectuera son travail d'injection.

Désormais si l'on applique l'attribut Auditable sur une de nos entités comme suit :
[Auditable]
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }

        public Context()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
        }
    }

Ce qui produit bien en base la table suivante :

table

et dans l'assembly finale (vu avec Reflector)

assembly

Évidemment, en généralisant ce principe il est possible de facilement faire évoluer votre modèle sans avoir à travailler parfois de manière répétitive.

Vous retrouverez cette fonctionnalité (il vous suffit de marquer votre entité avec l'attribut) dans la librairie EntityFramework.Patterns que je maintiens et disponible via nuget.

Que pensez-vous de cette technique?
Utilisez-vous un autre framework (Postsharp) pour réaliser ce genre de tâche?

4 commentaires:

Rui a dit… @ 7 octobre 2011 à 17:55

sentiment : Top!

remarques:
- le tissage post compil est vraiment très pratique car le fait de pouvoir greffer simplement des choses sur un bon vieux legacy pourri c'est très bien.
- par contre j'ai un pb avec, c'est que en cas de pb en prod tu as des bins qui ne sont pas iso avec ton source et cela peut poser parfois des pb, mais bon...
- tu tapes ton entité avec un attribut, très bien, je pense que cela correspond à la façon logique de faire. Tu gardes un bon niveau d'abstraction et de découplage, et si par ex demain tu veux rajouter un timestamp ou guid (soyons fous) tu change pas tes entités, juste ton interface d'injection. Mais je me dit que vu que tu edites quand meme le code de ton entité en lui collant un attribut et qu'au final rajouter de la complexité (tout le monde ne parle pas aop) alors que tu peux aussi ajouter les membres directement.
- du coup pour ton exemple, le cas bien précis d'une entité code first, on ne pourrait pas coller l'attribut auditable sur le DbContext? l'idée serait plutot de dire par le biais de l'attirbut de contexte que "toutes les entités de mon contexte sont auditables". Puis au tissage, le bouzin prends tous les dbset du contexte et leur colle les propriétés d'audition.

thoughts ?


Et bonne découverte de cette lib car je trouve la gestion des licences et le mode de fonctionnement trop intrusif chez postsharp...
vu que j'ai pas essayé, comment fonctionne le tissage? c'est une tache post/pré build?

merci!

Thomas JASKULA a dit… @ 7 octobre 2011 à 17:57

Il reste encore une autre manière de faire de l'AOP que tu n'as pas mentionné. C'est l'interception avec ton containeur IoC préféré (ceux qui le supporte).

Ca l'air très compliqué avec EF par rapport à ce qu'on peut faire avec NHibernate mais néomoins si les entités dont tu parles sont dans la couche data (et non domaine avec POCO), je trouve que la solution que tu proposes est assez clean.

Personnellement en déhors du sujet que tu traites, je n'aime pas la modification du code Post compilation car cela créé quelques problèmes comme par exemple le vendor lock et n'incite pas à avoir un design clean, qui dans le cas d'autres méthodes AOP doit être imposé (sinon pas possible de la faire).

Fabrice Michellonet a dit… @ 7 octobre 2011 à 22:32

@Rui
Effectivement, s'il faut intervenir sur des binaires qui ont subit un tissage, cela devient plus compliqué a tracer/debugger; surtout si l'on est pas au courant.

Concernant l'application de l'attribut sur le DBContext ou sur l'assembly directement, c'est effectivement l'étape logique qui vient juste après. Je n'en ai pas parlé dans l'article pour pas trop encombré et perdre les lecteurs dans des "détails". Je voulais déjà faire passer l'idée.

Le tissage avec Afterthought est accompli lors du post build. Il te faut ajouter la commande suivante dans le post build event :

Afterthought.Amender "$(TargetPath)"

Simple est concis, non?

Fabrice Michellonet a dit… @ 7 octobre 2011 à 22:39

@Thomas :
Quand je parlais de "passer par un proxy dynamique" ( ex : Castle Dynamic Proxy), j'avais derrière la tête les mécanismes d'interception tels que l'implémentation de IInterceptor pour Castle.

Je suis loin d'être un expert de Nhibernate, mais effectivement il m'a semblé que ce genre de chose se faisait plus facilement qu'avec EF.
Je te rassure, les entités dont je parlais sont effectivement a placer dans la couche Data :)

Enregistrer un commentaire