Proxies dinâmicos usando Emitting (avançado) – Parte 2/3

Publicado em 14/05/2011

2


Olá pessoal, como estamos?!

No post anterior, iniciei a apresentar as etapas que percorri para construção de uma classe utilitária para geração, on-the-fly, de proxies. Trata-se de mais um exemplo prático (e útil) de emitting.

No lugar de apresentar definições técnicas e explanações sobre tecnologia, apresento como a classe foi desenvolvida. Assim, compartilho, além da tecnologia, a forma como desenvolvo.

Onde paramos?

Até aqui, desenvolvemos infraestrutura para criação de um proxy dinâmico simples que, simplesmente, reencaminha toda chamada de método que recebe para a implementação concreta. Observe:

image

Como descrito na figura:

  1. O código cliente faz uma chamada para o método do proxy (que foi gerado dinamicamente);
  2. O proxy reencaminha a chamada para a implementação concreta (que faz seu processamento);
  3. A implementação concreta retorna para o proxy;
  4. O proxy devolve o resultado obtido para o código cliente.

Simpático, mas nada útil.

O que vamos implementar hoje?

Vamos introduzir o conceito de “monitor”. Observe:

image

Na prática, antes de chamar a implementação concreta, faço um “aviso” para um “monitor” com o nome do método e parâmetros que estão sendo chamados. Além disso, antes de devolver o resultado para o código cliente, faço outro “aviso” para o “monitor”  com o nome do método e o resultado.

A implementação do monitor pode intervir na execução salvando logs, ou mesmo disparando exceptions.

Mãos na massa?

Etapa …6 – Criando o “monitor”

A idéia é permitir que os clientes de nossa pequena API possam criar “monitores” personalizados. Por isso, criamos uma interface mínima que descreva o comportamento que esperamos encontrar. Observe:

public interface IProxyMonitor
{
    void BeforeExecute(string methodName, object[] p);
    void AfterExecute(string methodName, object result);
}

São apenas dois métodos:

  1. BeforeExecute – chamado pelo proxy antes de repassar a execução para a implementação concreta. Esse método recebe dois parâmetros: o nome do método que está sendo executado e um vetor de objects com os valores dos parâmetros;
  2. AfterExecute – chamado pelo proxy antes de retornar para o código cliente. Esse método também recebe dois parametros: o nome do método que foi executado e um object com o resultado da execução.

Etapa 7 – Uma implementação “dummy” de monitor para ser utilizada nos testes

Antes de escrevermos testes para nosso proxy, crio uma implementação Dummy de monitor para utilizar como referência. Eis a implementação:

class DummyProxyMonitor : IProxyMonitor
{
    public string BeforeExecute_LastMethodName;
    public object[] BeforeExecute_LastParameters;

    public void BeforeExecute(string methodName, object[] p)
    {
        BeforeExecute_LastMethodName = methodName;
        BeforeExecute_LastParameters = p;
    }

    public string AfterExecute_LastMethodName;
    public object AfterExecute_LastResult;

    public void AfterExecute(string methodName, object result)
    {
        AfterExecute_LastMethodName = methodName;
        AfterExecute_LastResult = result;
    }
}

Minha implementação Dummy apenas registra as informações que recebe para servir como referência nos testes.

Etapa 8 – Um primeiro teste para AfterExecute: monitor acionado e recebendo valores corretos

No melhor espírito do TDD, começo toda a modificação do ProxyBuilder escrevendo um teste. Observe:

[Test]
public void CreateProxy_AfterExecute_Add()
{
    // arrange
    var foo = new Foo();
    var monitor = new DummyProxyMonitor();
    var target = ProxyBuilder.CreateProxy<IFoo>(
        foo,
        monitor
        );
    // act
    var result = target.Add(2, 3);
    // assert
    monitor.AfterExecute_LastMethodName.Should().Be("Add");
    monitor.AfterExecute_LastResult.Should().Be(5);
}

Esse código nem compila, pois ProxyBuilder não espera o monitor… Isso vai mudar Alegre

Etapa 9 – Modificando ProxyBuilder para dar suporte a “monitores”

Para começar, vamos modificar um pouco a assinatura do método CreateProxy. Além disso, já vamos adicionar um atributo no proxy que estamos gerando para armazenar a instância do monitor relacionado. Observe:

public static T CreateProxy<T>(
    T concreteInstance, 
    IProxyMonitor monitor = null
    )
{
    var t = IL.NewType().Implements<T>();

    EmitConcreteInstanceSupport<T>(t);
    EmitProxyMonitorSupport(t, monitor);

    foreach (var method in typeof(T).GetMethods())
        EmitMethod(t, method);

    return CreateInstance<T>(concreteInstance, t);
}

private static void EmitProxyMonitorSupport(
    DynamicTypeInfo t, 
    IProxyMonitor monitor
    )
{
    if (monitor == null) return;

    t
        .WithField("__proxymonitor", typeof(IProxyMonitor))
        .WithMethod("__SetProxyMonitor")
        .WithParameter(typeof(IProxyMonitor))
        .Returns(typeof(void))
            .Ldarg(0)
            .Ldarg(1)
            .Stfld("__proxymonitor")
            .Ret();
}

O teste continua não passando, mas já compila. Além disso, o IL que estamos emitindo já está um pouco diferente. Observe:

.class NewType5d217ee7-a7bf-4a78-a303-6a7d3d2ab1fc
implements DynamicProxy.Tests.IFoo
.field (DynamicProxy.Tests.IFoo) __concreteinstance
.method __SetConcreteInstance
.param (1) [DynamicProxy.Tests.IFoo] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __concreteinstance
    ret
.field (DynamicProxy.IProxyMonitor) __proxymonitor
.method __SetProxyMonitor
.param (1) [DynamicProxy.IProxyMonitor] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __proxymonitor
    ret
.method MethodWithNoParameters
returns System.Void
    ldarg.0
    ldfld __concreteinstance
    call Void MethodWithNoParameters()
    ret
.method Add
.param (1) [System.Int32] a
.param (2) [System.Int32] b
returns System.Int32
    ldarg.0
    ldfld __concreteinstance
    ldarg.1
    ldarg.2
    call Int32 Add(Int32, Int32)
    ret

Perfeito… mas, ainda temos que fazer nosso teste passar…

Etapa 10 – Gerando chamadas para AfterExecute com o nome do método e o valor do retorno (sempre)

Nosso teste espera que AfterExecute seja chamado, com o nome do método que foi executado (Add) e o retorno gerado pela implementação concreta (5). Eis a modificação no Emitting do método para que isso ocorra:

private static T CreateInstance<T>(
    DynamicTypeInfo t,
    T concreteInstance,
    IProxyMonitor monitor = null
    )
{
    var type = t.AsType;
    var result = (T)Activator.CreateInstance(type);

    var setupConcreteInstance = type.GetMethod("__SetConcreteInstance");
    setupConcreteInstance.Invoke(result, new object[] { concreteInstance });

    if (monitor != null)
    {
        var setupProxyMonitor = type.GetMethod("__SetProxyMonitor");
        setupProxyMonitor.Invoke(result, new object[] { monitor });
    }
    return result;
}

private static void EmitMethod(
    DynamicTypeInfo t, 
    MethodInfo method,
    IProxyMonitor monitor = null
    )
{
    var ilmethod = t.WithMethod(method.Name);
    foreach (var param in method.GetParameters())
        ilmethod.WithParameter(
            param.ParameterType,
            param.Name
            );

    if (monitor != null)
        ilmethod.WithVariable(method.ReturnType);
            
    var body = ilmethod
        .Returns(method.ReturnType);

    if (monitor != null)
        body
            .Ldarg(0).Dup()
            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            ;

    body
        .Ldarg(0)
        .Ldfld("__concreteinstance");

    foreach (var param in method.GetParameters())
        body.Ldarg(param.Name);

    body
        .Call(method);

    if (monitor != null)
    {
        var afterExecuteMi = typeof(IProxyMonitor)
            .GetMethod("AfterExecute");

        body
            .Stloc(0).Ldloc(0)
            .Call(afterExecuteMi)
            .Ldloc(0);
    }

    body.Ret();
}

Basicamente, verifico se foi especificado um monitor. Em caso positivo, passo esse monitor para o proxy dinâmico e altero o emitting dos métodos para chamar o “AfterExecute”.

O pseudo-IL gerado no teste ficou assim:

.class NewType32f43943-4daa-4393-8835-8dedbbe8f12c
implements DynamicProxy.Tests.IFoo
.field (DynamicProxy.Tests.IFoo) __concreteinstance
.method __SetConcreteInstance
.param (1) [DynamicProxy.Tests.IFoo] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __concreteinstance
    ret
.field (DynamicProxy.IProxyMonitor) __proxymonitor
.method __SetProxyMonitor
.param (1) [DynamicProxy.IProxyMonitor] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __proxymonitor
    ret
.method MethodWithNoParameters
.local (0) [System.Void] no-name
returns System.Void
    ldarg.0
    dup
    ldfld __proxymonitor
    ldstr "MethodWithNoParameters"
    ldarg.0
    ldfld __concreteinstance
    call Void MethodWithNoParameters()
    stloc.0
    ldloc.0
    box System.Void
    call Void AfterExecute(System.String, System.Object)
    ldloc.0
    ret
.method Add
.param (1) [System.Int32] a
.param (2) [System.Int32] b
.local (0) [System.Int32] no-name
returns System.Int32
    ldarg.0
    dup
    ldfld __proxymonitor
    ldstr "Add"
    ldarg.0
    ldfld __concreteinstance
    ldarg.1
    ldarg.2
    call Int32 Add(Int32, Int32)
    stloc.0
    ldloc.0
    box System.Int32
    call Void AfterExecute(System.String, System.Object)
    ldloc.0
    ret

Como você pode perceber, a implementação para Add ficou coerente. Entretanto, a implementação de MethodWithNoParameters parece equivocada, já que ela tenta manutenir um retorno void… Mas isso é tema de outro teste.

Etapa 11 – Escrevendo um teste para AfterExecute em métodos que retornam void

Como indicado acima, o emitting para métodos com retorno void ficou um pouco estranho. Na prática, desejo que AfterExecute receba null quando estiver sendo executado um método com retorno void. Eis o teste que escrevi para demonstrar isso:

[Test]
public void CreateProxy_AfterExecute_MethodWithNoParameters()
{
    // arrange
    var foo = new Foo();
    var monitor = new DummyProxyMonitor();
    var target = ProxyBuilder.CreateProxy<IFoo>(
        foo,
        monitor
        );
    // act
    target.MethodWithNoParameters();
    // assert
    monitor.AfterExecute_LastMethodName.Should().Be("MethodWithNoParameters");
    monitor.AfterExecute_LastResult.Should().Be(null);
}

Como esperado, o teste falha. JIT falha ao tentar executar o código que emitimos.

Etapa 12 – Ajustando a chamada de AfterExecute na execução de métodos void

Para fazer o teste passar, modifiquei o emitting dos métodos para considerar o tipo do retorno. Em caso de métodos void, passo null para AfterExecute. Observe:

private static void EmitMethod(
    DynamicTypeInfo t, 
    MethodInfo method,
    IProxyMonitor monitor = null
    )
{
    var ilmethod = t.WithMethod(method.Name);
    foreach (var param in method.GetParameters())
        ilmethod.WithParameter(
            param.ParameterType,
            param.Name
            );

    if (monitor != null && method.ReturnType != typeof(void))
        ilmethod.WithVariable(method.ReturnType);
            
    var body = ilmethod
        .Returns(method.ReturnType);

    if (monitor != null)
        body
            .Ldarg(0).Dup()
            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            ;

    body
        .Ldarg(0)
        .Ldfld("__concreteinstance");

    foreach (var param in method.GetParameters())
        body.Ldarg(param.Name);

    body
        .Call(method);

    if (monitor != null)
    {
        var afterExecuteMi = typeof(IProxyMonitor)
            .GetMethod("AfterExecute");

        if (method.ReturnType != typeof(void))
            body
                .Stloc(0)
                .Ldloc(0)
                .Box(method.ReturnType);
        else
            body.Ldnull();
                

        body
            .Call(afterExecuteMi);
                    
        if (method.ReturnType != typeof(void))    
            body.Ldloc(0);
    }

    body.Ret();
}

Bonito, embora esse método já esteja ficando “grande demais” (bad smell). Teste agora passa.

O pseudo-IL gerado na execução do teste ficou assim:

 

.class NewTypee362ab79-005d-4c09-8944-b8c1cdd2e94c
implements DynamicProxy.Tests.IFoo
.field (DynamicProxy.Tests.IFoo) __concreteinstance
.method __SetConcreteInstance
.param (1) [DynamicProxy.Tests.IFoo] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __concreteinstance
    ret
.field (DynamicProxy.IProxyMonitor) __proxymonitor
.method __SetProxyMonitor
.param (1) [DynamicProxy.IProxyMonitor] no-name
returns System.Void
    ldarg.0
    ldarg.1
    stfld __proxymonitor
    ret
.method MethodWithNoParameters
returns System.Void
    ldarg.0
    dup
    ldfld __proxymonitor
    ldstr "MethodWithNoParameters"
    ldarg.0
    ldfld __concreteinstance
    call Void MethodWithNoParameters()
    ldnull
    call Void AfterExecute(System.String, System.Object)
    ret
.method Add
.param (1) [System.Int32] a
.param (2) [System.Int32] b
.local (0) [System.Int32] no-name
returns System.Int32
    ldarg.0
    dup
    ldfld __proxymonitor
    ldstr "Add"
    ldarg.0
    ldfld __concreteinstance
    ldarg.1
    ldarg.2
    call Int32 Add(Int32, Int32)
    stloc.0
    ldloc.0
    box System.Int32
    call Void AfterExecute(System.String, System.Object)
    ldloc.0
    ret

Etapa 13 – Testando o nome do método passado para BeforeExecute

Já temos suporte bacana para AfterExecute. Agora vamos começar a implementar o suporte para BeforeExecute. Inicialmente, desejamos saber se o método está sendo chamado com o nome correto do método que está sendo executado. Observe o teste:

[Test]
public void CreateProxy_BeforeExecute_MethodWithNoParameters()
{
    // arrange
    var foo = new Foo();
    var monitor = new DummyProxyMonitor();
    //Assert.Fail(string.Format("LastResult {0}", monitor.AfterExecute_LastResult));
    var target = ProxyBuilder.CreateProxy<IFoo>(
        foo,
        monitor
        );
    // act
    target.MethodWithNoParameters();
    // assert
    monitor.BeforeExecute_LastMethodName.Should().Be("MethodWithNoParameters");
    //monitor.AfterExecute_LastResult.Should().Be(null);
}

Obviamente, como nosso emitting nem toma conhecimento de BeforeExecute, esse código falha (alegremente). ShowTime!

Etapa 14 – Chamando BeforeExecute passando o nome do método em execução (no parameters)

Não há muito o que dizer, mãos na massa:

private static void EmitMethod(
    DynamicTypeInfo t, 
    MethodInfo method,
    IProxyMonitor monitor = null
    )
{
    var ilmethod = t.WithMethod(method.Name);
    foreach (var param in method.GetParameters())
        ilmethod.WithParameter(
            param.ParameterType,
            param.Name
            );

    if (monitor != null && method.ReturnType != typeof(void))
        ilmethod.WithVariable(method.ReturnType);
            
    var body = ilmethod
        .Returns(method.ReturnType);

    if (monitor != null)
    {
        var beforeExecuteMi = typeof(IProxyMonitor)
            .GetMethod("BeforeExecute");

        body
            .Ldarg(0).Dup().Dup()
            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            .Newarr(typeof(object), 0)
            .Call(beforeExecuteMi)

            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            ;
    }

    body
        .Ldarg(0)
        .Ldfld("__concreteinstance");

    foreach (var param in method.GetParameters())
        body.Ldarg(param.Name);

    body
        .Call(method);

    if (monitor != null)
    {
        var afterExecuteMi = typeof(IProxyMonitor)
            .GetMethod("AfterExecute");

        if (method.ReturnType != typeof(void))
            body
                .Stloc(0)
                .Ldloc(0)
                .Box(method.ReturnType);
        else
            body.Ldnull();
                

        body
            .Call(afterExecuteMi);
                    
        if (method.ReturnType != typeof(void))    
            body.Ldloc(0);
    }

    body.Ret();
}

Repare que tomo o cuidado de inicializar um vetor vazio para passar como segundo parâmetro. Green bar!

Etapa 15 – Testando a relação de valores de parâmetros passados para BeforeExecute

BeforeExecute precisa receber os valores dos parâmetros que foram passados para a função do Proxy. Eis o teste onde verificamos isso:

[Test]
public void CreateProxy_BeforeExecute_Add()
{
    // arrange
    var foo = new Foo();
    var monitor = new DummyProxyMonitor();
    //Assert.Fail(string.Format("LastResult {0}", monitor.AfterExecute_LastResult));
    var target = ProxyBuilder.CreateProxy<IFoo>(
        foo,
        monitor
        );
    // act
    target.Add(2, 3);
    // assert
    monitor.BeforeExecute_LastMethodName.Should().Be("Add");
    monitor.BeforeExecute_LastParameters.Should().Have.SameSequenceAs(
        2, 3);
}

Obviamente, como nosso emitting sempre gera um vetor vazio, este teste está falhando (Lindo!)

Etapa 16 – Passando os valores dos parâmetros de uma chamada para BeforeExecute

Lá vamos nós, outra vez, alterar o Emitting do método para, agora, passar um vetor com os valores dos parâmetros para BeforeExecute. Observe:

private static void EmitMethod(
    DynamicTypeInfo t, 
    MethodInfo method,
    IProxyMonitor monitor = null
    )
{
    var ilmethod = t.WithMethod(method.Name);
    foreach (var param in method.GetParameters())
        ilmethod.WithParameter(
            param.ParameterType,
            param.Name
            );

    var parameters = method.GetParameters();
    if (monitor != null)
    {
        if (method.ReturnType != typeof(void))
            ilmethod.WithVariable(method.ReturnType);

        if (parameters.Length > 0)
            ilmethod.WithVariable(typeof(object[]), "parameters");
    }
            

    var body = ilmethod
        .Returns(method.ReturnType);

            
    if (monitor != null)
    {
        var beforeExecuteMi = typeof(IProxyMonitor)
            .GetMethod("BeforeExecute");

        body
            .Ldarg(0).Dup().Dup()
            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            .Newarr(typeof(object), parameters.Length);

        if (parameters.Length > 0)
        {
            body
                .Stloc("parameters");
            for (int i = 0; i < parameters.Length; i++)
            {
                body
                    .Ldloc("parameters")
                    .Ldc(i)
                    .Ldarg((uint)(i + 1))
                    .Box(parameters[i].ParameterType)
                    .Emit(OpCodes.Stelem_Ref);
            }
            body.Ldloc("parameters");
        }

        body
            .Call(beforeExecuteMi)
            .Ldfld("__proxymonitor")
            .Ldstr(method.Name)
            ;
    }

    body
        .Ldarg(0)
        .Ldfld("__concreteinstance");

    foreach (var param in parameters)
        body.Ldarg(param.Name);

    body
        .Call(method);

    if (monitor != null)
    {
        var afterExecuteMi = typeof(IProxyMonitor)
            .GetMethod("AfterExecute");

        if (method.ReturnType != typeof(void))
            body
                .Stloc(0)
                .Ldloc(0)
                .Box(method.ReturnType);
        else
            body.Ldnull();
                

        body
            .Call(afterExecuteMi);
                    
        if (method.ReturnType != typeof(void))    
            body.Ldloc(0);
    }

    body.Ret();
}

Pronto! Adicionada a lógica para  gerar um vetor com os parâmetros e passar para BeforeExecute.

O pseudo-IL gerado ficou assim:

.class NewTypedbbd1882-aa2e-44c9-b331-ba3999d9bb6b
implements DynamicProxy.Tests.IFoo
.field (DynamicProxy.Tests.IFoo) __concreteinstance
.method __SetConcreteInstance
.param (1) [DynamicProxy.Tests.IFoo] no-name
returns System.Void
	ldarg.0
	ldarg.1
	stfld __concreteinstance
	ret
.field (DynamicProxy.IProxyMonitor) __proxymonitor
.method __SetProxyMonitor
.param (1) [DynamicProxy.IProxyMonitor] no-name
returns System.Void
	ldarg.0
	ldarg.1
	stfld __proxymonitor
	ret
.method MethodWithNoParameters
returns System.Void
	ldarg.0
	dup
	ldfld __proxymonitor
	ldstr "MethodWithNoParameters"
	ldc.i4.0
	newarr System.Object
	call Void BeforeExecute(System.String, System.Object[])
	ldfld __proxymonitor
	ldstr "MethodWithNoParameters"
	ldarg.0
	ldfld __concreteinstance
	call Void MethodWithNoParameters()
	ldnull
	call Void AfterExecute(System.String, System.Object)
	ret
.method Add
.param (1) [System.Int32] a
.param (2) [System.Int32] b
.local (0) [System.Int32] no-name
.local (1) [System.Object[]] parameters
returns System.Int32
	ldarg.0
	dup
	ldfld __proxymonitor
	ldstr "Add"
	ldc.i4.2
	newarr System.Object
	stloc.1
	ldloc.1
	ldc.i4.0
	ldarg.1
	box System.Int32
	stelem.ref
	ldloc.1
	ldc.i4.1
	ldarg.2
	box System.Int32
	stelem.ref
	ldloc.1
	call Void BeforeExecute(System.String, System.Object[])
	ldfld __proxymonitor
	ldstr "Add"
	ldarg.0
	ldfld __concreteinstance
	ldarg.1
	ldarg.2
	call Int32 Add(Int32, Int32)
	stloc.0
	ldloc.0
	box System.Int32
	call Void AfterExecute(System.String, System.Object)
	ldloc.0
	ret

Pronto! Adicionamos suporte completo a nosso Monitor. Entretanto, temos um tremendo Bad Smell que é o tamanho do método EmitMethod (gigante). Hora de refactoring … mas isso fica para o terceiro (e último) post dessa (mini) série.

Por hoje, era isso.

Smiley piscando

Publicado em: Emitting, Post