AOP, C# e PostSharp
Diciamo che sono partito con una necessità decisamente triviale ma che illustra bene le potenzialità della cosa. Lo scopo è far funzionare questo codice:
class MyTestClass { public void Foo( [NotNull]String arg ) { Console.WriteLine( "Foo:" + arg ); } }e fare in modo di ottenere una ArgumentNullException( ‘arg’ ) se il metodo Foo() viene invocato così:
MyTestClass obj = new MyTestClass(); obj.Foo( null );Nello specifico NotNullAttribute è un normalissimo attributo .net, senza nulla di particolare:
[AttributeUsage( AttributeTargets.Parameter )] public class NotNullAttribute : Attribute {questo è un tipico scenario in cui AOP (Aspect Oriented Programming) ci può essere di grandissimo aiuto, una implementazione dei paradigmi AOP per .net è formita proprio da PostSharp; partiamo dal codice che è più semplice:
}
[Serializable] public class ArgumentValidatorAspect : OnMethodInvocationAspect { public override void OnInvocation( MethodInvocationEventArgs context ) { context.Proceed(); } }definiamo una nostra classe che deriva da OnMethodInvocationAspect, classe definita nel framework di PostSharp, e ci limitiamo a fare l’override del metodo OnInvocation().
Due cose sono degne di nota:
- la nostra classe è a tutti gli effetti un attributo: OnMethodInvocationAspect infatti alla lontana deriva da Attribute;
- la nostra classe deve essere marcata con l’attributo Serializable, vedremo di seguito il motivo, e questo comporta tutta una serie di possibili problematiche e di considerazioni che devono essere fatte quando si sviluppa un “Aspect” con PostSharp;
[ArgumentValidatorAspect()] class MyTestClass { public void Foo( [NotNull]String arg ) { Console.WriteLine( "Foo:" + arg ); } }possiamo decorare la nostra classe di test con l’attributo appena definito. Possiamo quindi realizzare una semplice applicazione console che testa il tutto:
class Program { static void Main( string[] args ) { MyTestClass tc = new MyTestClass(); tc.Foo( "Hello World!" ); } }Una volta che abbiamo configurato Visual Studio per “usare” PostSharp, vedremo dopo cosa comporta, possiamo compilare il nostro semplicissimo progetto e premere F5 quello che otteniamo è esattamente quello che ci asspettiamo:
Se però siamo curiosi e andiamo ad indagare con Reflector che cosa contiene il nostro assembly scopriamo delle cose decisamente interessanti:
La nostra classe infatti contiene qualcosa che non ci aspetteremmo di trovare: contiene il metodo pubblico Foo() che abbiamo definito noi e contiene anche un metodo privato ~Foo(), se andiamo a vedere il corpo dei metodi scopriamo che il nostro codice sta nel metodo privato e non in quello pubblico:
Che cosa è successo? la cosa è abbastanza semplice:
- il primo step è quello di configurare Visual Studio aggiungendo al progetto i target di MSBuild di PostSharp: l’help di PostSharp è decisamente esauastivo nei passaggi da seguire;
- Quando lanciate una build al termine della compilazione vengono invocati i task di PostSharp che in questo caso non fanno altro che:
- disassemblare l’assembly prodotto;
- andare alla ricerca dei tipi marcati con l’attributo “ArgumentValidatorAspect”;
- modificare il codice dei tipi in modo che le chiamate vengano redirette all’aspect;
- istanziare il vostro aspect e serializzarlo, ecco perchè è necessario che sia Serializable, e infine salvare l’aspect serializzato nelle risorse;
- rigenerare l’assembly e completare la build;
- Foo();
- Aspect:
- Aspect –> Proceed();
- ~Foo();
[AttributeUsage( AttributeTargets.Parameter )] public class NotNullAttribute : Attribute { public void Validate( ParameterInfo parameter, Object value ) { if( value == null ) { throw new ArgumentNullException( parameter.Name ); } } }e il nostro aspect cosà:
[Serializable] public class ArgumentValidatorAspect : OnMethodInvocationAspect { public override void OnInvocation( MethodInvocationEventArgs context ) { //L'array di valori passati al metodo object[] values = context.GetArgumentArray(); //Il nome del metodo pubblico: PostSharp genera un metodo private con anteposta la tilde String publicMethodName = context.Delegate.Method.Name.Substring( 1 ); //Un rifermento al metodo pubblico MethodInfo mi = context.Delegate.Method.DeclaringType.GetMethod( publicMethodName ); //Lambda, lambda e lambda ;-) mi.GetParameters() .Where( pi => pi.IsAttributeDefined<NotNullAttribute>() ) .Select( pi => new { Parameter = pi, Attribute = pi.GetAttribute<NotNullAttribute>(), Value = values[ pi.Position ] } ) .ForEach( tupla => tupla.Attribute.Validate( tupla.Parameter, tupla.Value ) ); context.Proceed(); } }scopriamo che il seguente codice funziona esattamente come ci aspettiamo:
try { MyTestClass tc = new MyTestClass(); tc.Foo( null ); } catch( ArgumentNullException anex ) { Console.WriteLine( "Exception: {0}", anex.Message ); }Producendo questo risultato:
Bingo ;-)
Facciamo un po’ di commenti sul codice dell’apect perchè merita:
public override void OnInvocation( MethodInvocationEventArgs context )il metodo OnInvocation ci passa un’istanza di una classe MethodInvocationEventArgs che ci da accesso a tutte le informazioni relative al metodo che stiamo intercettando, attenzione che qui c’è uso di reflection, ma:
- a detta dell’autore viene usata una ed una sola volta e poi le informazioni cachate;
- nelle versioni future molte parti stanno per essere sostituite con generazione di codice MSIL custom in fase di build per superare le problematiche di performance di reflection a runtime;
//L'array di valori passati al metodo object[] values = context.GetArgumentArray(); //Il nome del metodo pubblico: PostSharp genera un metodo private con anteposta la tilde String publicMethodName = context.Delegate.Method.Name.Substring( 1 );
//Un rifermento al metodo pubblico MethodInfo mi = context.Delegate.Method.DeclaringType.GetMethod( publicMethodName );Le informazioni che possiamo quindi recuperare sono ad esempio:
- l’array dei valori dei parametri in ingresso al metodo: e qui possiamo comiciare ad immaginarci tutti gli scenari di logging e tracing semiautomatico…;
- un’istanza della classe MethodInfo che ci da accesso a tutte le informazioni sul metodo chiamato. Come probabilmente avrete notato il codice fa un’operazione apparentemente strana:
- abbiamo decorato il parametro con l’attributo “NotNull”;
- PostSharp in fase di build ha rinominato il nostro metodo trasformandolo in privato e anteponendo al nome la tilde;
- quello che però non ha fatto è stato rimettere sul parametro del metodo privato l’attributo, e questo credo sia un bug, vedremo…;
- quindi in questo momento quello che dobbiamo fare per recuperare il nostro parametro è un giro un po’ arzigogolato:
- recuperare prima il nome del metodo pubblico: quello privato senza la tilde;
- recuperare un riferimento al metodo pubblico;
- cercare l’eventuale presenza dei parametri e invocare la validazione;
mi.GetParameters() .Where( pi => pi.IsAttributeDefined<NotNullAttribute>() ) .Select( pi => new { Parameter = pi, Attribute = pi.GetAttribute<NotNullAttribute>(), Value = values[ pi.Position ] } ) .ForEach( tupla => tupla.Attribute.Validate( tupla.Parameter, tupla.Value ) );Facendo uso di un po’ di sane Lambda e di fluent interface riusciamo a scrivere anche del codice elegante per la validazione, anche qui potremmo fare alchimie notevoli per renderlo ulteriormente flessibile magari introducendo un attributo di base ma non è oggetto del nostro test ;-). ndr: ForEach
Le potenzialità sono notevoli, quello che si pò fare si può certamente fare anche in altri mille modi uno su tutti usando ad esempio i concetti di facility e interception messi a disposizione dai framewrok di inversion of control ma in questo caso con il vincolo che il componente che volete intercettare sia gestito dal framewrok di IoC e questo non è detto che sia possibile.
PostSharp ci mette ad disposzione un modello che ci consente di fare intercept anche di qualcosa che non abbiamo scritto noi:
[assembly: MyOnMethodInvocationAspect(mi limito a riportare un semplice e esempio lasciando a voi l’onere/onore di capirne le potenzialità ;-)
AttributeTargetAssemblies = "mscorlib",
AttributeTargetTypes = "System.Threading.*" )]
Un’altra cosa decisamente interessante che si può fare è usare PostSharp per fare validazione a Compile Time, si avete capito bene a Compile Time ed eventualmente “rompere” la build sulla base di considerazione fatte da vostro codice custom che viene invocato dal compilatore… e questo mi stuzzica decisamente di più ;-)
public class MySampleAspect : OnMethodInvocationAspect { public override bool CompileTimeValidate( MethodBase method ) { Message error = new Message( SeverityType.Error, "ErrorCode", "Error message.", "Error Location" ); MessageSource.MessageSink.Write( error ); return false; }Interessante, decisamente… qui concentrerò i miei prossimi esperimenti.
}
.m