Programação assíncrona
Interceptando métodos assíncronos usando a interceptação do Unity
Fernando Simonazzi
Grigori Melnik
O Unity (não confundir com o mecanismo de jogos Unity3D) é um contêiner de injeção de dependência extensível de finalidade geral com o suporte de interceptação para uso em qualquer tipo de aplicativo baseado no Microsoft .NET Framework. O Unity foi projetado e é mantido pela equipe de padrões e práticas da Microsoft (microsoft.com/practices). Ele pode ser facilmente adicionado ao seu aplicativo via NuGet, e você encontrará o principal hub de recursos de aprendizagem relacionados ao Unity em msdn.com/unity.
Este artigo se concentra na interceptação do Unity. Interceptação é uma técnica útil quando você quer modificar o comportamento de objetos individuais sem afetar o comportamento de outros objetos da mesma classe, bem como você faria ao usar o padrão Decorador (a definição da Wikipedia para o padrão Decorador pode ser encontrada aqui: bit.ly/1gZZUQu). A intercepção fornece uma abordagem flexível para adicionar novos comportamentos a um objeto no tempo de execução. Esses comportamentos normalmente resolvem algumas preocupações transversais, como registro em log ou validação de dados. A intercepção é geralmente usada como o mecanismo subjacente de programação orientada a aspecto (AOP). O recurso de interceptação no tempo de execução do Unity permite que você efetivamente intercepte chamadas de métodos para objetos e execute o pré e pós-processamento dessas chamadas.
A intercepção no contêiner do Unity tem dois componentes principais: interceptores e comportamentos de interceptação. Os interceptores determinam o mecanismo usado para interceptar as chamadas para métodos no objeto interceptado, enquanto os comportamentos de interceptação determinam as ações que são executadas nas chamadas de método interceptadas. Um objeto interceptado é fornecido com um pipeline de comportamentos de interceptação. Quando uma chamada de método é interceptada, cada comportamento no pipeline é autorizado a inspecionar e até mesmo modificar os parâmetros da chamada de método e, no final, a implementação do método original é invocada. Ao retornar, cada comportamento pode inspecionar ou substituir os valores retornados ou as exceções lançadas pela implementação original ou o comportamento anterior no pipeline. Por fim, o chamador original recebe o valor de retorno resultante, se houver, ou a exceção resultante. A Figura 1 ilustra o mecanismo de interceptação.
Figura 1 Mecanismo de interceptação do Unity
Existem dois tipos de técnicas de intercepção: interceptação de instância e interceptação de tipo. Com a interceptação de instância, o Unity cria dinamicamente um objeto proxy que é inserido entre o cliente e o objeto de destino. O objeto proxy é, então, responsável por passar as chamadas efetuadas pelo cliente para o objeto de destino por meio dos comportamentos. Você pode usar a interceptação de instância do Unity para interceptar objetos criados pelo contêiner do Unity e fora do contêiner, e você pode usá-lo para interceptar métodos virtuais e não virtuais. No entanto, você não pode converter o tipo de proxy criado dinamicamente no tipo de objeto de destino. Com a interceptação de tipo, o Unity cria dinamicamente um novo tipo que deriva do tipo de objeto de destino e isso inclui os comportamentos que lidam com preocupações transversais. O contêiner do Unity instancia objetos do tipo derivado no tempo de execução. A interceptação de instância só pode interceptar métodos de instância públicos. A interceptação de tipo pode interceptar métodos virtuais públicos e protegidos. Tenha em mente que, devido às limitações da plataforma, a interceptação do Unity não aceita o desenvolvimento de aplicativos para Windows Phone e Windows Store, embora o contêiner central do Unity aceite.
Você pode encontrar a cartilha do Unity, “Dependency Injection with Unity” (Microsoft patterns & practices, 2013), em amzn.to/16rfy0B. Para obter mais informações sobre a interceptação no contêiner do Unity, consulte o artigo da Biblioteca do MSDN, “Interception using Unity”, em bit.ly/1cWCnwM.
Interceptando métodos assíncronos TAP (padrão assíncrono baseado em tarefas)
O mecanismo de interceptação é bastante simples, mas o que acontece se o método interceptado representa uma operação assíncrona que retorna um objeto Task? De certa forma, nada muda: um método é invocado e retorna um valor (o objeto Task) ou lança uma exceção, então, ele pode ser interceptado como qualquer outro método. Mas você provavelmente está interessado em lidar com o resultado real da operação assíncrona em vez do Task que o representa. Por exemplo, você pode registrar valor de retorno de Task ou lidar com qualquer exceção que Task possa produzir.
Felizmente, ter um objeto real que representa o resultado da operação torna a interceptação desse padrão assíncrono relativamente simples. Outros padrões assíncronos são um pouco mais difíceis de interceptar: No modelo de programação assíncrona (bit.ly/ICl8aH) dois métodos representam uma única operação assíncrona, enquanto que no padrão assíncrono baseado em evento (bit.ly/19VdUWu) as operações assíncronas são representadas por um método para iniciar a operação e um evento associado para sinalizar sua conclusão.
Para realizar a interceptação da operação TAP assíncrona, você pode substituir o Task retornado pelo método por um novo Task que executa o pós-processamento necessário depois que a tarefa original é concluída. Os chamadores do método interceptado receberão o novo Task correspondente à assinatura do método e observarão o resultado da implementação do método interceptado, mais qualquer processamento extra que o comportamento de interceptação execute.
Vamos desenvolver um exemplo de implementação da abordagem básica para interceptar operações TAP assíncronas em que queremos registrar em log a conclusão de operações assíncronas. Você pode adaptar este exemplo para criar seus próprios comportamentos que possam interceptar operações assíncronas.
Caso simples
Vamos começar com um caso simples: interceptação de métodos assíncronos que retornam Task não genérico. Precisamos ser capazes de detectar que o método interceptado retorna um Task e substituir esse Task por um novo que execute o registro em log apropriado.
Podemos usar o comportamento de intercepção “no op” ilustrado na Figura 2 como ponto de partida.
Figura 2 Interceptação simples
public class LoggingAsynchronousOperationInterceptionBehavior
: IInterceptionBehavior
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// Execute the rest of the pipeline and get the return value
IMethodReturn value = getNext()(input, getNext);
return value;
}
#region additional interception behavior methods
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public bool WillExecute
{
get { return true; }
}
#endregion
}
Em seguida, adicionamos o código para detectar os métodos que retornam Task e substituímos o Task retornado por um novo wrapper Task que registra em log o resultado. Para conseguir isso, o CreateMethodReturn no objeto de entrada é chamado para criar um novo objeto IMethodReturn representando um wrapper Task criado pelo novo método CreateWrapperTask no comportamento, como ilustrado na Figura 3.
Figura 3 Retornando um Task
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// Execute the rest of the pipeline and get the return value
IMethodReturn value = getNext()(input, getNext);
// Deal with tasks, if needed
var method = input.MethodBase as MethodInfo;
if (value.ReturnValue != null
&& method != null
&& typeof(Task) == method.ReturnType)
{
// If this method returns a Task, override the original return value
var task = (Task)value.ReturnValue;
return input.CreateMethodReturn(this.CreateWrapperTask(task, input),
value.Outputs);
}
return value;
}
O novo método CreateWrapperTask retorna um Task que aguarda a conclusão do Task original e registra em log o resultado, como ilustrado na Figura 4. Se a tarefa resultar em uma exceção, o método a relançará depois de registrá-la. Observe que essa implementação não altera o resultado do Task original, mas um comportamento diferente poderia substituir ou ignorar as exceções que o Task original possa apresentar.
Figura 4 Registrando o resultado em log
private async Task CreateWrapperTask(Task task,
IMethodInvocation input)
{
try
{
await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0}",
input.MethodBase.Name);
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}",
input.MethodBase.Name, e);
throw;
}
}
Lidando com genéricos
Lidar com métodos que retornam Task<T> é um pouco mais complexo, especialmente se você quiser evitar o impacto no desempenho. Vamos ignorar por enquanto o problema de descobrir o que é "T" e supor que já seja conhecido. Como a Figura 5 mostra, podemos escrever um método genérico que possa lidar com Task<T> para um "T" conhecido, usando os recursos de linguagem assíncrona disponíveis no C# 5.0.
Figura 5 Um método genérico para lidar com Task<T>
private async Task<T> CreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Como no caso simples, o método apenas registra em log, sem alterar o comportamento original. Mas como o Task concluído agora retorna um valor, o comportamento também pode substituir esse valor, se necessário.
Como podemos invocar esse método para obter o objeto Task substituto? Precisamos recorrer à reflexão, extraindo o T do tipo de retorno genérico do método interceptado, criando uma versão fechada desse método genérico para esse T e criando um delegado de fora dele e, por fim, invocando o delegado. Esse processo pode ser muito caro, por isso é uma boa ideia armazenar esses delegados em cache. Se o T faz parte da assinatura do método, não seríamos capaz de criar um delegado de um método e invocá-lo sem saber o T, então, vamos dividir nosso método anterior em dois métodos: um com a assinatura desejada e outro que se beneficia das características da linguagem C#, como ilustrado na Figura 6.
Figura 6 Dividindo o método de criação de delegado
private Task CreateGenericWrapperTask<T>(Task task, IMethodInvocation input)
{
return this.DoCreateGenericWrapperTask<T>((Task<T>)task, input);
}
private async Task<T> DoCreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Em seguida, vamos alterar o método de interceptação para usarmos o delegado correto para concluir a tarefa original, que nós obtemos invocando o novo método GetWrapperCreator e passando o tipo de tarefa esperado. Não precisamos de um caso especial para um Task não genérico, pois ele cabe na abordagem de delegado assim como o Task<T> genérico. A Figura 7 mostra o método Invoke atualizado.
Figura 7 Método Invoke atualizado
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
IMethodReturn value = getNext()(input, getNext);
var method = input.MethodBase as MethodInfo;
if (value.ReturnValue != null
&& method != null
&& typeof(Task).IsAssignableFrom(method.ReturnType))
{
// If this method returns a Task, override the original return value
var task = (Task)value.ReturnValue;
return input.CreateMethodReturn(
this.GetWrapperCreator(method.ReturnType)(task, input), value.Outputs);
}
return value;
}
Tudo o que resta é implementar o método GetWrapperCreator. Esse método efetuará as chamadas de reflexão caras para criar os delegados e usará um ConcurrentDictionary para armazená-las em cache. Esses delegados criadores do wrapper são do tipo Func<Task, IMethodInvocation, Task>; queremos chegar à tarefa original e ao objeto IMethodInvocation que representa a chamada para a invocação do método assíncrono e retornar um wrapper Task. Isso é mostrado na Figura 8.
Figura 8 Implementando o método GetWrapperCreator
private readonly ConcurrentDictionary<Type, Func<Task, IMethodInvocation, Task>>
wrapperCreators = new ConcurrentDictionary<Type, Func<Task,
IMethodInvocation, Task>>();
private Func<Task, IMethodInvocation, Task> GetWrapperCreator(Type taskType)
{
return this.wrapperCreators.GetOrAdd(
taskType,
(Type t) =>
{
if (t == typeof(Task))
{
return this.CreateWrapperTask;
}
else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>))
{
return (Func<Task, IMethodInvocation, Task>)this.GetType()
.GetMethod("CreateGenericWrapperTask",
BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(new Type[] { t.GenericTypeArguments[0] })
.CreateDelegate(typeof(Func<Task, IMethodInvocation, Task>), this);
}
else
{
// Other cases are not supported
return (task, _) => task;
}
});
}
Para o caso de Task não genérico, nenhuma reflexão é necessária e o método não genérico existente pode ser usado como o delegado desejado assim como está. Ao lidar com Task<T>, as chamadas de reflexão necessárias são efetuadas para criar o delegado correspondente. Por fim, não podemos dar suporte a qualquer outro tipo Task, pois não saberíamos como criá-lo, então, um delegado no-op que retorna apenas a tarefa original é retornado.
Esse comportamento pode agora ser usado em um objeto interceptado e registrará em log os resultados das tarefas retornados pelos métodos do objeto interceptado para os casos em que o valor é retornado e onde uma exceção é lançada. O exemplo na Figura 9 mostra como um contêiner pode ser configurado para interceptar um objeto e usar esse novo comportamento, e o resultado quando métodos diferentes são invocados.
Figura 9 Configurando um contêiner para interceptar um objeto e usar o novo comportamento
using (var container = new UnityContainer())
{
container.AddNewExtension<Interception>();
container.RegisterType<ITestObject, TestObject>(
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<LoggingAsynchronousOperationInterceptionBehavior>());
var instance = container.Resolve<ITestObject>();
await instance.DoStuffAsync("test");
// Do some other work
}
Output:
vstest.executionengine.x86.exe Information: 0 :
Successfully finished async operation DoStuffAsync with value: test
vstest.executionengine.x86.exe Warning: 0 :
Async operation DoStuffAsync threw:
System.InvalidOperationException: invalid
at AsyncInterception.Tests.AsyncBehaviorTests2.TestObject.<
DoStuffAsync>d__38.MoveNext() in d:\dev\interceptiontask\
AsyncInterception\AsyncInterception.Tests\
AsyncBehaviorTests2.cs:line 501
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.
HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at AsyncInterception.LoggingAsynchronousOperationInterceptionBehavior.<
CreateWrapperTask>d__3.MoveNext() in d:\dev\interceptiontask\
AsyncInterception\AsyncInterception\
LoggingAsynchronousOperationInterceptionBehavior.cs:line 63
Cobrindo nossos rastros
Como você pode ver no resultado da Figura 9, a abordagem usada nesta implementação resulta em uma leve mudança na pilha de rastreamento da exceção, o que reflete a forma como a exceção foi relançada quando aguardava a tarefa. Uma abordagem alternativa pode usar o método ContinueWith e um TaskCompletionSource<T> em vez da palavra-chave await para evitar esse problema, às custas de ter uma implementação mais complexa (e potencialmente mais cara), como a que é ilustrada na Figura 10.
Figura 10 Usando ContinueWith em vez da palavra-chave Await
private Task CreateWrapperTask(Task task, IMethodInvocation input)
{
var tcs = new TaskCompletionSource<bool>();
task.ContinueWith(
t =>
{
if (t.IsFaulted)
{
var e = t.Exception.InnerException;
Trace.TraceWarning("Async operation {0} threw: {1}",
input.MethodBase.Name, e);
tcs.SetException(e);
}
else if (t.IsCanceled)
{
tcs.SetCanceled();
}
else
{
Trace.TraceInformation("Successfully finished async operation {0}",
input.MethodBase.Name);
tcs.SetResult(true);
}
},
TaskContinuationOptions.ExecuteSynchronously);
return tcs.Task;
}
Conclusão
Falamos sobre várias estratégias para interceptar métodos assíncronos e as demonstramos em um exemplo que registra em log a conclusão de operações assíncronas. Você pode adaptar este exemplo para criar seus próprios comportamentos de interceptação com suporte para operações assíncronas. O código-fonte completo do exemplo está disponível em msdn.microsoft.com/magazine/msdnmag0214.
Fernando Simonazzi é desenvolvedor e arquiteto de software com mais de 15 anos de experiência profissional. Ele colabora com projetos de padrões e práticas da Microsoft, incluindo vários lançamentos do Enterprise Library, Unity, CQRS Journey e Prism. Simonazzi também é associado da Clarius Consulting.
Dr. Grigori Melnik é gerente principal de programas na equipe de padrões e práticas da Microsoft. Atualmente ele conduz os projetos de padrões Microsoft Enterprise Library, Unity, CQRS Journey e NUI. Antes disso, ele foi pesquisador e engenheiro de software por um bom tempo, o suficiente para lembrar da alegria da programação em Fortran. Dr. Melnik mantém um blog em blogs.msdn.com/agile.
Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Stephen Toub (Microsoft)