Redécouvrez les custom attributs avec PostSharp, 1re partie

Gael Fraiteur

Si vous pensez tout connaître sur les custom attributs, cet article est pour vous. J'y montrerai comment PostSharp vous ouvre de nouveaux horizons en vous permettant d'ajouter de nouveaux comportements à votre code. Apprenez comment encapsuler dans des custom attributs le traçage, l'instrumentation de la performance et la validation des champs.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Redécouvrez les custom attributs avec PostSharp, 1re partie

- Auteur : Gael Fraiteur

- Parution : 1 janvier 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Gael Fraiteur. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Le traçage c'est pénible !

- II. Un custom attribut trivial pour le traçage

- III. Déjà vu ?

- IV. Un pas plus loin : le multicasting des attributs

- V. Conclusion

I - Le traçage c'est pénible !

Le traçage, c'est pénible ! Il existe une multitude de bibliothèques pour le traçage, mais aucune ne vous épargne ce travail pénible : changer chaque méthode du code source pour qu'elle appelle cette bibliothèque. Et que devriez-vous faire si, au milieu du projet, vous choisissiez une nouvelle bibliothèque de traçage ? Il vous faudrait modifier toutes les méthodes tracées, et il peut en avoir des milliers ! Le traçage est pénible parce que, comme la plupart des besoins non fonctionnels (sécurité, transactions, cache…), il entrecroise tous les besoins fonctionnels. Si vous avez une centaine de processus métiers et que chacun doit être tracé, sécurisé et transactionnel, vous allez sans doute insérer dans l'implémentation de chacun de ces processus (donc dans une centaine d'objets) des instructions relatives au traçage, à la sécurité et aux transactions. Quel travail et, surtout, quel travail stupide ! C'est pourquoi il nous faut une meilleure façon d'encapsuler les fonctionnalités transversales (crosscutting concern), une façon qui ne nous force pas à modifier chaque méthode à laquelle ces fonctionnalités s'appliquent. Les custom attributs apportent une très bonne solution à ce problème.

II - Un custom attribut trivial pour le traçage

Ce que nous voulons est simple : un custom attribut qui écrit un message avant et après l'exécution des méthodes sur lequel il est appliqué. Nous voulons aussi spécifier la catégorie du message. Donc, idéalement, nous voudrions être capables d'utiliser le custom attribut comme suit :

[Trace("MyCategory")]
void SomeTracedMethod()
{
// Method body.
}

Maintenant que nous savons ce que nous voulons, au travail ! D'abord nous déclarons le custom attribut comme nous sommes habitués à le faire. Tout ce dont nous avons besoin est un champ nommé category et un constructeur initialisant ce champ :

public sealed class TraceAttribute : Attribute
{
private readonly string category;

public TraceAttribute( string category )
{
this.category = category;
}

public string Category { get { return category; } }
}

Pour l'instant, cet attribut n'est qu'une annotation purement informative. Nous pouvons ajouter ce qui va faire en sorte que cet attribut va effectivement modifier les méthodes auxquelles il est appliqué. Comme annoncé en introduction, nous utiliserons pour ce faire PostSharp, commençons donc par l'ajouter au projet :

Ajout des références PostSharp à notre projet

La seule chose à faire maintenant est de faire dériver l'attribut de PostSharp.Laos.OnMethodBoundaryAspect au lieu de System.Attribute. Cette classe définit des méthodes qui seront appelées lorsque les méthodes auxquelles il est appliqué seront exécutées :

- OnEntry - avant l'exécution de la méthode ;

- OnSuccess - lorsque l'exécution se termine avec succès ;

- OnException - lorsque l'exécution se solde par une exception ;

- OnExit - lorsque l'exécution se termine, avec succès ou exception.

Nous implémentons seulement les méthodes qui nous intéressent : OnEntry and OnExit.

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
// Add trace code here.
}

public override void OnExit(MethodExecutionEventArgs eventArgs)
{
// Add trace code here.
}

L'implémentation de ces méthodes devrait appeler System.Diagnostics.Trace.WriteLine. Mais comment pouvons-nous savoir le nom de la méthode dans laquelle nous sommes actuellement ? Pas de problème, toutes les informations nécessaires sont contenues dans l'objet MethodExecutionEventArgs, qui est passé à OnEntry et OnExit. Ce qui nous intéresse, c'est la propriété eventArgs.Method ; mais si nous voulions aussi imprimer la valeur des paramètres reçus, nous pourrions les obtenir grâce à la méthode GetArguments(). Voici l'implémentation finale de notre attribut de traçage :

[Serializable]
public sealed class TraceAttribute : OnMethodBoundaryAspect
{
private readonly string category;

public TraceAttribute( string category )
{
this.category = category;
}

public string Category { get { return category; } }

public override void OnEntry( MethodExecutionEventArgs eventArgs )
{
Trace.WriteLine(
string.Format( "Entering {0}.{1}.",
eventArgs.Method.DeclaringType.Name,
eventArgs.Method.Name ),
this.category );
}

public override void OnExit( MethodExecutionEventArgs eventArgs )
{
Trace.WriteLine(
string.Format( "Leaving {0}.{1}.",
eventArgs.Method.DeclaringType.Name,
eventArgs.Method.Name ),
this.category );
}
}

Avez-vous remarqué ? La classe est affectée de l'attribut Serializable. C'est requis pour tous les custom attributs de PostSharp Laos.
Essayons maintenant notre attribute sur un morceau de code :

internal static class Program
{
private static void Main()
{
Trace.Listeners.Add(new TextWriterTraceListener( Console.Out));

SayHello();
SayGoodBye();
}

[Trace( "MyCategory" )]
private static void SayHello()
{
Console.WriteLine("Hello, world." );
}

[Trace("MyCategory")]
private static void SayGoodBye()
{
Console.WriteLine("Good bye, world.");
}
}

Exécutons le programme et… abracadabra, les appels aux méthodes sont tracés !

Le résultat en image !

Comment cela fonctionne-t-il ? Si vous jetez un coup d'œil à la fenêtre de sortie (output window) de Visual Studio, vous verrez que PostSharp a été invoqué pendant le processus de construction du programme.

La sortie de Visual Studio après compilation

PostSharp a effectivement modifié la sortie du compilateur C# et a « amélioré » le programme de sorte que les méthodes de notre attribut de traçage soient appelées durant l'exécution du programme. Il est particulièrement intéressant d'inspecter le programme ainsi produit avec le Reflector de Lutz Roeder :

La sortie de reflector montre bien que du code a été ajouté

La sortie de reflector montre bien que du code a été ajouté

Comme vous voyez, notre méthode, à l'origine minuscule, est maintenant bien plus complexe. C'est parce que PostSharp a ajouté les instructions pour appeler notre attribut au début et à la fin de l'exécution.

III - Déjà vu ?

Si vous pensez que tout ceci est très similaire à la programmation orientée aspect (aspect-oriented programming, AOP), vous avez raison - PostSharp Laos n'est rien d'autre qu'un framework AOP.

Un aspect est défini dans Wikipedia comme « a part of a program that cross-cuts its core concerns, therefore violating its separation of concerns ». La plupart du temps, dans les applications métier, un aspect correspond à un besoin non fonctionnel comme le traçage, la sécurité, ou la gestion des transactions et des exceptions. La séparation des fonctionnalités (separation of concern) est l'un des principes de base en génie logiciel. Il stipule que les fragments de code implémentant la même fonction doivent être regroupés en composants les plus autonomes possible. Une mesure de la qualité de la conception d'un logiciel et la haute cohésion, mais le faible couplage de ses composants.

Les frameworks AOP rendent possible d'encapsuler les aspects dans des entités modulaires ; avec PostSharp Laos, ces entités sont des custom attributes. L'avantage principal de cette approche est sa simplicité : vous accédez à la programmation orientée aspect sans la courbe d'apprentissage qui lui est typique. De plus, PostSharp Laos est indépendant du langage (C#, VB.NET, J#…) et son intégration avec Visual Studio (Intellisense, débogueur…) est excellente.

Une autre caractéristique qui différencie PostSharp est qu'il opère au niveau du code intermédiaire (MSIL). Il n'est donc pas sujet aux limitations des solutions basées sur des proxys : vous pouvez ajouter des aspects sur des méthodes privées, les classes ne doivent pas être dérivées de MarshalByRefObject, etc.

Si AOP vous intéresse, vous pouvez également jeter un coup d'œil sur le Policy Injection Application Block de Microsoft Enterprise Library ou sur Spring .NET Framework, deux autres solutions AOP viables pour l'environnement .NET.

IV - Un pas plus loin : le multicasting des attributs

Super ! Nous avons un custom attribut de traçage. Mais que faire si nous avons des centaines de méthodes à tracer ? Devons-nous ajouter cet attribut à chaque méthode ? Bien sûr que non ! Grâce à une fonctionnalité appelée la propagation multiple - appelons-la par son petit nom : multicasting - il est possible d'appliquer un attribut à plusieurs méthodes en une seule ligne.

Par exemple, en ajoutant TraceAttribute sur la classe Program, nous appliquons en fait l'attribut sur chaque méthode de cette classe :

[Trace( "MyCategory" )]
internal static class Program
{
...

Et si nous ne voulons pas que l'attribut soit appliqué sur la méthode Main, nous pouvons restreindre l'ensemble des méthodes auxquelles l'attribut s'applique :

Trace( "MyCategory", AttributeTargetMembers = "Say*")]
internal static class Program
{
...

Autre possibilité, nous pouvons ajouter l'attribut au niveau racine de l'assemblage, et dans ce cas toutes les méthodes contenues dans l'assemblage seront tracées :

[assembly: Trace("MyCategory")]

Il est néanmoins nécessaire d'empêcher l'attribut d'être appliqué sur la classe TraceAttribute elle-même, parce que les aspects ne peuvent pas être eux-mêmes la cible d'aspects :

[Trace( null, AttributeExclude = true )]
[Serializable]
public sealed class TraceAttribute : OnMethodBoundaryAspect
{
...

V - Conclusion

J'ai essayé de montrer, dans cet article, comment développer des custom attributs qui ajoutent de nouveaux comportements à vos programmes .NET. Pour cette démonstration, j'ai utilisé PostSharp Laos, une solution pour la programmation orientée aspect sur .NET.

Avec trois ans d'existence, PostSharp est un projet qui a déjà atteint un certain stade de maturité. Au moment de rédiger cet article, PostSharp a déjà été téléchargé des milliers de fois. Il est actuellement en version 1.0 et entame la phase des release candidates (RC). PostSharp a déjà gagné la confiance de nombreuses entreprises, tant des sociétés de consultance que de développement logiciel. Le projet est porté par un développeur à temps plein qui offre du support, de la consultance et du développement sur mesure. Les erreurs sont la plupart du temps corrigées en une semaine.

Comme j'ai essayé de l'illustrer dans cet article, PostSharp modifie en fait les instructions MSIL de telle sorte que les aspects soient invoqués lors de l'exécution. On peut voir comment cela fonctionne en utilisant Reflector de Lutz Roeder. Cet article a seulement présenté un attribute qui ajoute un block try-catch autour des méthodes, mais il est également possible d'intercepter les appels faits dans un assemblage extérieur, d'intercepter les accès aux champs ou d'injecter de nouvelles interfaces dans les types.

Dans la seconde partie de cet article, je montrerai comment développer deux nouveaux attributs : un compteur de performance et un validateur de champ. D'ici là, bonne découverte de PostSharp !