Redécouvrez les custom attributs avec PostSharp, 2e partie

Gael Fraiteur

La seconde partie de cet article va dans le détail de l'implémentation de deux custom attributs qui ajoutent vraiment quelque chose à votre code : l'un pour la mesure de la performance et le second pour la validation des champs.

- Titre : Redécouvrez les custom attributs avec PostSharp, 2e 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. Les deux vies d'un custom attribut

- II. Un attribut pour mesurer la performance

- II-A. Mesurer la performance

- II-B. Lire la valeur des compteurs

- II-C. Mission accomplie. Essayons.

- III. Valider les champs avec des custom attributs

- III-A. Conception d'un framework abstrait

- III-B. Vérifier les champs non annulables

- III-C. Vérifier des expressions régulières

- III-D. Mission accomplie. Essayons.

- IV. Conclusion

I - Les deux vies d'un custom attribut

Avant de sauter dans l'implémentation des nouveaux attributs, plongeons-nous un instant dans le fonctionnement de PostSharp Laos. PostSharp est un rehausseur de code intermédiaire MSIL (MSIL enhancer) : il s'insère dans le processus de construction (MSBuild) et modifie la sortie du compilateur (C#, VB.NET, J#…). Il inspecte les attributs spécifiques à PostSharp Laos et modifie les méthodes, types et champs auxquels ces attributs sont appliqués.

Pour tirer le maximum de profit de PostSharp Laos, il est nécessaire de bien comprendre le cycle de vie de ses attributs. Ils ont réellement deux vies : une première lors de la compilation, à l'intérieur de PostSharp ; une deuxième lors de l'exécution.

Le cycle de vie des custom attributs de PostSharp Laos est le suivant :

Lors de la compilation

1. Pour chaque application de l'attribut, une nouvelle instance est créée. Donc, une instance d'un attribute est toujours assignée à une et une seule méthode, champ ou type. Ensuite, les instances sont initialisées (méthode CompileTimeInitialize) et validées (méthode CompileTimeValidate).

2. Les attributs sont sérialisés en un blob.

3. Ils sont stockés comme une ressource dans l'assemblage produit.

Lors de l'exécution

1. Les attributs sont désérialisés depuis la ressource et chaque instance est initialisée une seconde fois (méthode RuntimeInitialize).

2. Les méthodes " événement " (OnEntry, OnExit, .) sont invoquées lorsqu'est invoqué ou accédé la méthode ou le champ auxquelles elles sont appliquées.

Assez de théorie pour l'instant, voyons ce que cela signifie en pratique !

II - Un attribut pour mesurer la performance

Comment pouvez-vous surveiller la performance d'une application dans un environnement de production, où vous ne pouvez pas utiliser votre profileur favori ? Réponse : vous pouvez utiliser des compteurs de performance, mais cela demandera sans doute des changements importants dans le code existant ! La solution est d'encapsuler ce code additionnel dans un aspect, c'est-à-dire d'en faire un custom attribut, et ensuite d'appliquer cet attribut à chaque méthode ciblée.

Et que faire si vous désirez surveiller la performance de fonctions définies en dehors de l'assemblage courant ? Ce n'est pas un problème pour PostSharp d'appliquer des custom attributs sur des déclarations externes. Cependant, comme nous ne pouvons pas modifier ces méthodes, nous devons intercepter les appels vers les méthodes ciblées. Donc au lieu d'un aspect de type OnMethodBoundary, nous utiliserons OnMethodInvocation :

[Serializable]
public class PerformanceCounterAttribute : OnMethodInvocationAspect
{

public override void OnInvocation( MethodInvocationEventArgs eventArgs )
{
// Our implementation goes here.
}

}

Le paramètre eventArgs nous permet de connaître la méthode exécutée actuellement : eventArgs .Delegate est un délégué de la méthode interceptée, et eventArgs.GetArguments() nous donne ses arguments. PostSharp Laos attend que nous écrivions la valeur de retour dans eventArgs.ReturnValue. Nous pouvons donc appeler la méthode interceptée comme suit :

eventArgs.ReturnValue = eventArgs.Delegate.DynamicInvoke(eventArgs.GetArguments() );

II-A - Mesurer la performance

Notre compteur devrait compter le nombre d'invocations et mesurer le temps passé à l'intérieur de la méthode. Comme chaque instance de l'attribut PerformanceCounterAttribute est associée à une et une seule méthode interceptée, nous pouvons enregistrer les données de performance comme des champs de cet attribut.

Sur la base de ces principes, voici notre première implémentation :

[Serializable]
public class PerformanceCounterAttribute : OnMethodInvocationAspect
{
private long elapsedTicks;
private long hits;

public override void OnInvocation( MethodInvocationEventArgs eventArgs )
{
Stopwatch stopwatch = Stopwatch.StartNew();

try
{

eventArgs.ReturnValue = eventArgs.Delegate.DynamicInvoke(
eventArgs.GetArguments() );
}
finally
{
stopwatch.Stop();
Interlocked.Add( ref this.elapsedTicks, stopwatch.ElapsedTicks );
Interlocked.Increment( ref this.hits );
}
}
}

II-B - Lire la valeur des compteurs

Cela fonctionne, mais comment pouvons-nous lire la valeur des compteurs ? Nous devons évidemment exposer les données dans des propriétés publiques, mais cela ne suffit pas : comment découvrir la liste des compteurs existants ? Il suffit de tenir cette liste dans un champ statique et d'y enregistrer les instances d'attribut lorsqu'elles sont initialisées. Comme cette liste doit être disponible lors de l'exécution, et pas lors de la compilation, nous ne pouvons pas effectuer cet enregistrement à partir du constructeur (qui est invoqué lors de la compilation) ; nous devons le faire dans la méthode RuntimeInititialize(). Et une dernière chose : nous devons exposer l'identité de la méthode instrumentée, sinon comment pourrions-nous savoir à quelle méthode le compteur se rapporte ? Nous devons donc enregistrer la méthode cible dans un champ et l'exposer avec une propriété.

Voici le code que nous devons ajouter à notre attribut :

[NonSerialized] private MethodBase method;

private static readonly List<PerformanceCounterAttribute> instances =
new List<PerformanceCounterAttribute>();

public override void RuntimeInitialize( MethodBase method )
{
base.RuntimeInitialize( method );
this.method = method;
instances.Add( this );
}
public MethodBase Method { get { return this.method; } }

public double ElapsedMilliseconds
{
get { return this.elapsedTicks/( Stopwatch.Frequency/1000d ); }
}

public long Hits { get { return this.hits; } }

public static ICollection<PerformanceCounterAttribute> Instances
{
get
{
return new ReadOnlyCollection<PerformanceCounterAttribute>( instances );
}
}

II-C - Mission accomplie. Essayons.

C'est tout ! Nous pouvons maintenant appliquer notre attribute aux méthodes que nous souhaitons instrumenter. Supposons que nous voulions mesurer le temps passé dans l'espace System.IO ; nous appliquerions un compteur de performance à ces méthodes à l'aide de la ligne de code suivante :

Voici ce que cela donne avec un petit programme affichant le contenu d'un répertoire :

L'une des choses à laquelle il faut faire attention est que cet aspect intercepte seulement les appels faits depuis l'assemblage actuel. Donc, si vous appelez une méthode externe qui appelle indirectement une méthode instrumentée, cet appel indirect ne sera pas instrumenté. Cette limitation est inhérente à la technologie utilisée par PostSharp : la réécrititure du code binaire MSIL.

III - Valider les champs avec des custom attributs

Jusqu'à maintenant, nous avons vu comment modifier le corps des méthodes ou intercepter des appels de méthodes. PostSharp peut aussi intercepter les opérations de lecture et d'écriture sur les champs. L'une des applications de cette technique est la validation des champs : nous pouvons rendre un champ non annulable ou vérifier une expression régulière juste par l'application d'un custom attribut sur ce champ.

Les aspects qui désirent intercepter les accès aux champs doivent dériver de la classe OnFielAccessAspect. Ils peuvent implémenter les méthodes OnGetValue() et OnSetValue(). Pour la validation des champs, c'est uniquement la dernière possibilité qui nous intéresse. Tout ce que nous devons faire dans cette méthode est d'effectuer la validation spécifique au validateur.

III-A - Conception d'un framework abstrait

Le concept de ce framework de validation est simple : à la base, nous avons une classe abstraite FieldValidationAttribute qui expose une méthode abstraite Validate(). Cette méthode est appelée depuis OnSetValue(). Par contrat, l'implémentation de Validate() doit émettre une exception adéquate si la valeur n'est pas valide. Pour composer un message d'erreur suffisamment informatif, ce serait bien si la classe FieldValidationAttribute exposait le nom du champ sur lequel l'attribut a été appliqué. Comme cette information est déjà connue lors de la compilation, elle est initialisée dans la méthode CompileTimeInitialize() et enregistrée dans un champ sérialisable de l'aspect.

Nous ne pouvons pas oublier que, comme OnMethodInvocationAspect, OnFieldAspect intercepte les accès au champ, et est donc limité à l'assemblage courant. Si vous suivez les recommandations de Microsoft et avez seulement des champs privés, ce n'est pas un problème. Mais si vous utilisez des champs publics, vous devez demander à PostSharp Laos d'encapsuler les champs dans une propriété. Il suffit d'implémenter la méthode GetOptions() et de retourner GenerateProperty (dans les versions postérieures à 1.0 RC1, c'est inutile, car les propriétés sont générées automatiquement).

Voici le code complet de la classe abstraite FieldValidationAttribute.

[Serializable]
[AttributeUsage( AttributeTargets.Field, AllowMultiple = false )]
public abstract class FieldValidationAttribute : OnFieldAccessAspect
{
private string fieldName;

public override void CompileTimeInitialize( FieldInfo field )
{
base.CompileTimeInitialize( field );

this.fieldName = field.DeclaringType.Name + "." + field.Name;
}

public string FieldName { get { return this.fieldName; } }

protected abstract void Validate( object value );

public override sealed void OnSetValue( FieldAccessEventArgs eventArgs )
{
this.Validate( eventArgs.ExposedFieldValue );

base.OnSetValue( eventArgs );
}

public override OnFieldAccessAspectOptions GetOptions()
{
return OnFieldAccessAspectOptions.GenerateProperty;
}

}

III-B - Vérifier les champs non annulables

L'aspect " champ non annulable " est trivial :

[Serializable]
public sealed class FieldNotNullAttribute : FieldValidationAttribute
{
protected override void Validate( object value )
{
if ( value == null )
throw new ArgumentNullException( "field " + this.FieldName );
}
}

Définir un champ non annulable est si simple que cela :

class MyClass
{
[FieldNotNull]
public string Name = "DefaultName";
}

III-C - Vérifier des expressions régulières

Un cas plus stimulant est de concevoir un custom attribut qui vérifie une expression régulière. Le constructeur de l'attribut doit accepter l'expression régulière en elle-même de même, facultativement ; qu'une valeur indiquant si les valeurs nulles sont acceptables.

Si nous voulons éviter de devoir recompiler l'expression régulière lors de chaque assignement, nous pouvons enregistrer l'objet Regex dans un champ et l'initialiser lors de l'exécution, dans la méthode RuntimeInitialize().

Voici une implémentation de base, mais fonctionnelle de notre attribut vérifiant une expression régulière :

[Serializable]
public sealed class FieldRegexAttribute : FieldValidationAttribute
{
private readonly string pattern;
private readonly bool nullable;
private RegexOptions regexOptions = RegexOptions.Compiled;

[NonSerialized]
private Regex regex;

public FieldRegexAttribute(string pattern, bool nullable)
{
this.pattern = pattern;
this.nullable = nullable;
}
public FieldRegexAttribute(string pattern) : this(pattern, false)
{
}

public RegexOptions RegexOptions
{
get { return regexOptions; }
set { regexOptions = value; }
}

public override void RuntimeInitialize(FieldInfo field)
{
base.RuntimeInitialize(field);
this.regex = new Regex( this.pattern, this.regexOptions);
}

protected override void Validate( object value )
{
if ( value == null )
{
if ( !nullable )
{
throw new ArgumentNullException("field " + this.FieldName);
}
}
else
{
string str = (string) value;
if ( !this.regex.IsMatch( str ))
{
throw new ArgumentException(
"The value does not match the expected pattern.");
}
}
}
}

III-D - Mission accomplie. Essayons.

C'est tout ! En quelques lignes de code, nous avons développé des attributs qui valident les champs sur lesquels ils sont appliqués !

Leur usage est très simple :

class MyClass
{
[FieldNotNull]
public string Name = "DefaultName";

[FieldRegex(@"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$")]
public string EmailAddress;
}

Inspectons maintenant le résultat avec Reflector de Lutz Roeder's :

Les champs ont été encapsulés dans des propriétés, et si vous regardez l'implémentation des accesseurs, vous verrez que les méthodes de nos attributs ont été invoquées.

Simple. Puissant. Que pourrions-nous vouloir de plus ?

IV - Conclusion

La première partie de cet article introduisait la plupart des concepts clés de PostSharp Laos sur un exemple simple basé sur OnMethodBoundaryAspect ; cette deuxième partie a décrit deux nouveaux aspects : OnMethodInvocation et OnFieldAccess.

Nous avons vu la différence entre OnMethodBoundaryAspect et OnMethodInvocationAspect : alors que le premier ajoute en fait un bloc try-catch à la méthode cible, le second intercepte les appels de méthode et ne modifie pas la méthode cible. Cela permet d'appliquer OnMethodInvocationAspect même sur les méthodes définies en dehors de l'assemblage courant. Le premier exemple a exploité cette fonctionnalité en mesurant le temps passé dans l'espace System.IO.

Le second exemple a illustré comment ajouter de nouveaux comportements aux accès aux champs. Nous avons aussi vu comment générer une propriété à partir d'un champ, de sorte que ces nouveaux comportements sont également appelés à partir d'autres assemblages.

Mais surtout, j'espère vous avoir convaincu que nous pouvons reconsidérer la façon dont nous approchons les problèmes transversaux : la programmation orientée aspects apporte une solution élégante pour la plupart et PostSharp Laos constitue une technologie simple et puissante.

Et maintenant, regardez les trois derniers projets auxquels vous avez participé et réfléchissez combien d'effort vous pourriez économiser grâce à PostSharp.