Programação assíncrona

Interceptando métodos assíncronos usando a interceptação do Unity

Fernando Simonazzi
Grigori Melnik

Baixar o código de exemplo

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.

Unity Interception MechanismFigura 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)