è da un po’ di tempo che mi riprometto di fare delle prove e capire come funziona il gioiellino.
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;
Fatto ciò possiamo fare una cosa molto interessante:
[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;
A runtime succede che quando invocate il metodo Foo() in realtà state invocando il nuovo metodo Foo() che, utilizzando l’aspect deserializzato, è in grado di redirigere la chiamata prima all’aspect e poi dall’aspect verso il vero metodo ~Foo():
- Foo();
- Aspect:
- Aspect –> Proceed();
- ~Foo();
A questo punto penso che anche a voi si sia accesa la lampadine e abbiate detto: mumble mumble… ma se definiamo il nostro attributo così:
[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<T>(), IsAttributeDefined<T>() e GetAttribute<T>() sono degli extension methods miei.
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(
AttributeTargetAssemblies = "mscorlib",
AttributeTargetTypes = "System.Threading.*" )]
mi limito a riportare un semplice e esempio lasciando a voi l’onere/onore di capirne le potenzialità ;-)
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
Technorati Tags:
AOP,
PostSharp