è 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:

  1. la nostra classe è a tutti gli effetti un attributo: OnMethodInvocationAspect infatti alla lontana deriva da Attribute;
  2. 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:

image

Se però siamo curiosi e andiamo ad indagare con Reflector che cosa contiene il nostro assembly scopriamo delle cose decisamente interessanti:

image

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:

 image

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():

  1. Foo();
  2. Aspect:
    1. Aspect –> Proceed();
  3. ~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:

image

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: ,