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:
Como descrito na figura:
- O código cliente faz uma chamada para o método do proxy (que foi gerado dinamicamente);
- O proxy reencaminha a chamada para a implementação concreta (que faz seu processamento);
- A implementação concreta retorna para o proxy;
- 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:
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:
- 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;
- 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 ![]()
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.
![]()






maio 15th, 2011 → 19:48
[...] segundo post, mostrei como introduzir um “monitor” que seria “avisado” toda vez que um método fosse [...]
março 28th, 2012 → 23:30
[...] também mostrei como criar proxies dinamicamente (em três posts – 1/3, 2/3 e [...]