Entity Framework et AOP

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 : Amendment<T, T> { public AuditableAmender() { Properties.Add("CreatedBy"); Properties.Add("Created"); Properties.Add("UpdatedBy"); Properties.Add<DateTime?>("Updated"); Implement(); } }

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 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 Products { get; set; } public Context() { Database.SetInitializer(new DropCreateDatabaseIfModelChanges()); } }

Ce qui produit bien en base la table suivante :

et dans l'assembly finale (vu avec Reflector)

É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?