Junho de 2016

Volume 31 - Número 6

Essential .NET - Injeção de Dependência com o .NET Core

De Mark Michaelis

Mark MichaelisEm meus dois últimos artigos, “Fazendo logon no .NET Core” (msdn.com/magazine/mt694089) e “Configuração no .NET Core” (msdn.com/magazine/mt632279), demonstrei como a funcionalidade do .NET Core pode ser aproveitada a partir de um projeto ASP.NET Core (project.json) e do projeto .NET 4.6 C# mais comum (*.csproj). Em outras palavras, aproveitar a nova estrutura não está limitado às pessoas que estão escrevendo projetos do ASP.NET Core. Nesta coluna, continuarei a me aprofundar no .NET Core, com um foco nas capacidades da injeção de dependência (DI) do .NET Core e como elas permitem uma inversão do padrão de controle (IoC). Como antes, é possível aproveitar a funcionalidade do .NET Core a partir dos arquivos CSPROJ “tradicionais” e dos projetos do tipo project.json que surgem. Para ver um código de exemplo, desta vez usarei o XUnit de um projeto project.json.

Por que Injeção de Dependência?

Com o .NET, é simples instanciar um objeto com uma chamada para o construtor através do novo operador (ou seja, o novo MyService ou qualquer tipo de objeto que você deseja instanciar). Infelizmente, uma chamada desse tipo força uma conexão com acoplamento rígido (uma referência codificada) do código do cliente (ou aplicativo) com o objeto instanciado, junto com uma referência para seu pacote assembly/NuGet. Isso não é um problema para os tipos .NET comuns. Contudo, para os tipos que oferecem um “serviço”, como fazer logon, configuração, pagamento, notificação ou até DI, a dependência poderá ser indesejada se você quiser trocar a implementação do serviço usado. Por exemplo, em um cenário, um cliente pode usar NLog para fazer logon, ao passo que em outro, ele pode querer usar Log4Net ou Serilog. Além disso, o cliente que usar NLog preferirá não poluir seu projeto com Serilog, portanto, uma referência para ambos os serviços de logon seria indesejável.

Para resolver o problema da codificação de uma referência para uma implementação do serviço, a DI fornece um nível de indireção de modo que, em vez de instalar diretamente o serviço com o novo operador, o cliente (ou aplicativo) solicitará uma coleção de serviços ou “fábrica” para a instância. E mais, em vez de solicitar à coleção de serviços um tipo específico (assim criando uma referência rigidamente acoplada), você solicitará uma interface (como ILoggerFactory) com a expectativa de que o provedor do serviço (neste caso, NLog, Log4Net ou Serilog) implementará a interface.

O resultado é que, embora o cliente faça uma referência direta do assembly abstrato (Logging.Abstractions), definindo a interface do serviço, não serão necessárias referências para a implementação direta.

Chamamos de Inversão de Controle o padrão de desacoplamento da instância real retornada ao cliente. Isso acontece porque, em vez de o cliente determinar o que é instanciado, como ocorre ao chamar explicitamente o construtor com o novo operador, a DI determina o que será retornado. A DI registra uma associação entre o tipo solicitado pelo cliente (geralmente uma interface) e o tipo que será retornado. E mais, a DI normalmente determina o tempo de vida do tipo retornado, especificamente, havendo uma única interface compartilhada entre todas as solicitações para o tipo, uma nova instância de cada solicitação ou algo intermediário.

Uma necessidade especialmente comum para a DI está nos testes da unidade. Considere um serviço do carrinho de compras que, por sua vez, depende de um serviço de pagamento. Imagine gravar o serviço do carrinho de compras que aproveita o serviço de pagamento e tentar fazer o teste de unidade do serviço do carrinho sem realmente chamar um serviço de pagamento real. O que você deseja chamar é um serviço de pagamento fictício. Para conseguir isso com a DI, seu código solicitaria à estrutura da DI uma instância da interface do serviço de pagamento, em vez de chamar, por exemplo, o novo PaymentService. Portanto, tudo que será necessário para o teste de unidade é “configurar” a estrutura da DI para retornar um serviço de pagamento fictício.

Em oposição, o host de produção poderia configurar o carrinho de compras para usar uma (possivelmente muitas) das opções do serviço de pagamento. E, talvez o mais importante, as referências seriam apenas para a abstração do pagamento, em vez de cada implementação específica.

Fornecer uma instância do “serviço” em vez de fazer com que o cliente instancie diretamente é o princípio fundamental da DI. E, de fato, algumas estruturas da DI permitem um desacoplamento do host a partir da referência da implementação dando suporte a um mecanismo de associação baseado na configuração e reflexão, em vez de uma associação durante a compilação. Esse desacoplamento é conhecido como padrão do localizador de serviços.

.NET Core Microsoft.Extensions.DependencyInjection

Para aproveitar a estrutura da DI do .NET Core, tudo que você precisa é de uma referência para o pacote NuGet Microsoft.Extensions.DependencyInjection.Abstractions. Isso fornece acesso à interface IServiceCollection, que expõe um System.IService­Provider a partir do qual você pode chamar GetService<TService>. O parâmetro de tipo, TService, identifica o tipo do serviço a recuperar (geralmente uma interface), assim o código do aplicativo obtém uma instância:

ILoggingFactory loggingFactor = serviceProvider.GetService<ILoggingFactory>();

Há métodos GetService equivalentes e não genéricos que têm Type como um parâmetro (em vez de um parâmetro genérico). Os métodos genéricos permitem uma atribuição direta para uma variável de determinado tipo, ao passo que as versões não genéricas requerem um cast explícito porque o tipo de retorno é Object. E mais, há restrições genéricas ao adicionar o tipo de serviço de modo que um cast possa ser inteiramente evitado ao usar o parâmetro de tipo.

Se nenhum tipo for registrado com o serviço de coleção ao chamar GetService, ele retornará null. Isso é útil quando combinado ao operador de propagação nula para adicionar comportamentos opcionais ao aplicativo. O método GetRequiredService, que é semelhante, gera uma exceção quando o tipo de serviço não está registrado.

Como você pode ver, o código é muito simples. Contudo, o que está faltando é como obter uma instância do provedor de serviço na qual invocar GetService. A solução é simplesmente instanciar primeiro o construtor padrão do ServiceCollection e, em seguida, registrar o tipo que você deseja que o serviço forneça. Um exemplo é mostrado na Figura 1, na qual você pode supor que cada classe (Host, Application e PaymentService) é implementada em assemblies separados. E mais, embora o assembly Host saiba quais agentes usar, não há referências para os agentes em Application nem em PaymentService. Do mesmo modo, o assembly Host não tem referências para o assembly PaymentServices. As interfaces também são implementadas em assemblies de “abstração” separados. Por exemplo, a interface ILogger é definida no assembly Microsoft.Extensions.Logging.Abstractions.

Figura 1 Registrando e Solicitando um Objeto na Injeção de Dependência

public class Host
{
  public static void Main()
  {
    IServiceCollection serviceCollection = new ServiceCollection();
    ConfigureServices(serviceCollection);
    Application application = new Application(serviceCollection);
    // Run
    // ...
  }
  static private void ConfigureServices(IServiceCollection serviceCollection)
  {
    ILoggerFactory loggerFactory = new Logging.LoggerFactory();
    serviceCollection.AddInstance<ILoggerFactory>(loggerFactory);
  }
}
public class Application
{
  public IServiceProvider Services { get; set; }
  public ILogger Logger { get; set; }
    public Application(IServiceCollection serviceCollection)
  {
    ConfigureServices(serviceCollection);
    Services = serviceCollection.BuildServiceProvider();
    Logger = Services.GetRequiredService<ILoggerFactory>()
            .CreateLogger<Application>();
    Logger.LogInformation("Application created successfully.");
  }
  public void MakePayment(PaymentDetails paymentDetails)
  {
    Logger.LogInformation(
      $"Begin making a payment { paymentDetails }");
    IPaymentService paymentService =
      Services.GetRequiredService<IPaymentService>();
    // ...
  }
  private void ConfigureServices(IServiceCollection serviceCollection)
  {
    serviceCollection.AddSingleton<IPaymentService, PaymentService>();
  }
}
public class PaymentService: IPaymentService
{
  public ILogger Logger { get; }
  public PaymentService(ILoggerFactory loggerFactory)
  {
    Logger = loggerFactory?.CreateLogger<PaymentService>();
    if(Logger == null)
    {
      throw new ArgumentNullException(nameof(loggerFactory));
    }
    Logger.LogInformation("PaymentService created");
  }
}

Você pode considerar conceitualmente o tipo ServiceCollection como um par de nome-valor, em que o nome é o tipo de um objeto (geralmente uma interface) que você desejará recuperar mais tarde e o valor é o tipo que implementa a interface ou o algoritmo (representante) para recuperar esse tipo. A chamada para AddInstance, no método Host.Configure­Services na Figura 1, portanto, registra que qualquer solicitação do tipo ILoggerFactory retorna a mesma instância LoggerFactory criada no método ConfigureServices. Como resultado, Application e PaymentService podem recuperar ILoggerFactory sem nenhum conhecimento (ou mesmo uma referência do assembly/NuGet) de quais agentes estão implementados e configurados. Do mesmo modo, o aplicativo fornece um método MakePayment sem nenhum conhecimento de qual serviço de pagamento está sendo usado.

Observe que ServiceCollection não fornece diretamente os métodos GetService ou GetRequiredService. Ao contrário, esses métodos estão disponíveis no IServiceProvider que é retornado do método ServiceCollection.BuildServiceProvider. E mais, os únicos serviços disponíveis no provedor são os adicionados antes da chamada para BuildServiceProvider.

Microsoft.Framework.DependencyInjection.Abstractions também inclui uma classe auxiliar estática denominada ActivatorUtilities que fornece alguns métodos úteis para lidar com os parâmetros do construtor que não estão registrados no IServiceProvider, um representante ObjectFactory personalizado ou nas situações em que você deseja criar uma instância padrão no caso de uma chamada para GetService retornar null (consulte bit.ly/1WIt4Ka#ActivatorUtilities).

Tempo de Vida do Serviço

Na Figura 1, invoco o método de extensão IServiceCollection AddInstance<TService>(TService implementationInstance). Instância é uma das quatro diferentes opções disponíveis do tempo de vida do TService com a DI do .NET Core. Ela estabelece que não só a chamada para GetService retornará um objeto do tipo TService, como também que é a implementationInstance específica registrada em AddInstance que será retornada. Em outras palavras, registrar em AddInstance salva a instância implementationInstance específica para que ela possa ser retornada com cada chamada para GetService (ou GetRequiredService) com o parâmetro do tipo TService do método AddInstance.

Em oposição, o método de extensão IServiceCollection AddSingleton<TService> não tem parâmetros para uma instância e conta com TService tendo um meio de instanciar via construtor. Enquanto um construtor padrão funciona, Microsoft.Extensions.Dependency­Injection também dá suporte aos construtores não padrão cujos parâmetros também estão registrados. Por exemplo, você pode chamar:

IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()

e a DI cuidará da recuperação da instância concreta ILoggingFactory e irá aproveitá-la ao instanciar a classe PaymentService que requer ILoggingFactory em seu construtor.

Se não houver tais meios disponíveis no tipo TService, você poderá aproveitar a sobrecarga do método de extensão AddSingleton, que requer um representante do tipo Func<IServiceProvider, TService> implementationFactory, um método de fábrica para instanciar TService. Se você fornece um método de fábrica ou não, a implementação da coleção de serviços assegura que apenas criará uma instância do tipo TService, assegurando assim que haja um tipo de instância. Seguindo a primeira chamada para GetService que inicializa a instanciação TService, a mesma instância sempre será retornada durante o tempo de vida da coleção de serviços.

IServiceCollection também inclui os métodos de extensão AddTransient(Type serviceType, Type implementationType) e AddTransient(Type serviceType, Func<IServiceProvider, TService> implementationFactory). São parecidos com AddSingleton, exceto que retornam uma nova instância toda vez que são chamados, assegurando que você sempre terá uma nova instância do tipo TService.

Por último, há vários métodos de extensão do tipo AddScoped. Esses métodos são designados para retornar a mesma instância dentro de certo contexto e criar uma nova instância sempre que o contexto, conhecido como escopo, muda. O comportamento do ASP.NET Core mapeia conceitualmente o tempo de vida com escopo. Basicamente, uma nova instância é criada para cada instância HttpContext e sempre que GetService é chamada na mesma HttpContext, a instância TService idêntica é retornada.

Em resumo, há quatro opções de tempo de vida para os objetos retornados a partir da implementação da coleção de serviços: Instance, Singleton, Transient e Scoped. As três últimas são definidas no enum ServiceLifetime (bit.ly/1SFtcaG). Porém, falta Instance, porque é um caso especial de Scoped no qual o contexto não muda.

Anteriormente, fiz referência a ServiceCollection como sendo parecida conceitualmente com um par de nome-valor com o tipo TService servindo como a pesquisa. A implementação real do tipo ServiceCollection é feita na classe ServiceDescription (confira bit.ly/1SFoDgu). Essa classe fornece um contêiner para as informações necessárias para instanciar TService, principalmente o representante ServiceType (TService), Implementation­Type ou ImplementationFactory junto com ServiceLifetime. Além dos construtores ServiceDescriptor, existem vários métodos de fábrica estáticos em ServiceDescriptor que ajudam a instanciar o próprio ServiceDescriptor.

Independentemente de com qual tempo de vida você registra seu TService, o próprio TService deve ser um tipo de referência, não um tipo de valor. Sempre que você usar um parâmetro de tipo para TService (em vez de passar Type como um parâmetro), o compilador verificará isso com uma restrição da classe genérica. Porém, algo que não é verificado é o uso de um TService do tipo objeto. Você desejará evitar isso, juntamente com qualquer outra interface não exclusiva (como IComparable, talvez). O motivo é que se você registrar algo do tipo objeto, não importa qual TService você especifica na chamada GetService, o objeto registrado como um tipo TService será sempre retornado.

Injeção de Dependência para a Implementação DI

O ASP.NET aproveita a DI a ponto de você realmente poder usar a DI dentro da própria estrutura da DI. Em outras palavras, você não está limitado a usar a implementação ServiceCollection do mecanismo da DI encontrado em Microsoft.Extensions.DependencyInjection. Ao contrário, contanto que você tenha classes que implementem IServiceCollection (definida em Microsoft.Extensions.DependencyInjection.Abstractions. Confira bit.ly/1SKdm1z) ou IServiceProvider (definido no namespace System da estrutura da biblioteca .NET Core), você poderá substituir sua própria estrutura da DI ou aproveitar uma das outras estruturas bem estabelecidas da DI, incluindo Ninject (ninject.org, com um agradecimento a @IanfDavis por seu trabalho em manter isso durante anos) e Autofac (autofac.org).

Uma Palavra sobre ActivatorUtilities

Microsoft.Framework.DependencyInjection.Abstractions também inclui uma classe auxiliar estática que fornece alguns métodos úteis ao lidar com os parâmetros do construtor que não estão registrados no IServiceProvider, um representante ObjectFactory personalizado ou nas situações em que você deseja criar uma instância padrão no caso de uma chamada para GetService retornar null. Você pode encontrar exemplos de onde essa classe utilitária é usada na estrutura MVC e na biblioteca SignalR. No primeiro caso, existe um método com uma assinatura de CreateInstance<T> (IServiceProvider provider, params object[] parameters) que permite passar os parâmetros do construtor para um tipo registrado na estrutura da DI para os argumentos que não estão registrados. Você também tem um requisito de desempenho que diz que as funções lambda exigem que seus tipos sejam gerados como lambdas compiladas. O método CreateFactory(Type instanceType, Type[] argumentTypes) que retorna ObjectFactory pode ser útil nesse caso. O primeiro argumento é o tipo procurado por um consumidor e o segundo argumento são todos os tipos de construtor, em ordem, que correspondem ao construtor do primeiro tipo que você deseja usar. Em sua implementação, estas partes estão condensadas em um lambda compilado que terá um grande rendimento quando chamado várias vezes. Finalmente, o método GetServiceOrCreateInstance<T>(IServiceProvider provider) oferece um modo fácil de fornecer uma instância padrão de um tipo que pode ter sido registrado opcionalmente em um local diferente. É especialmente útil no caso em que você permite a DI antes da chamada, mas se isso não ocorrer, você terá uma implementação de fallback.

Conclusão

Como no Logon e na Configuração do .NET Core, o mecanismo da DI do .NET Core fornece uma implementação relativamente simples de sua funcionalidade. Embora seja pouco provável que você encontre uma funcionalidade de DI mais avançada de algumas outras estruturas, a versão .NET Core é leve e uma ótima maneira de começar. E mais (novamente, como no Logon e na Configuração), a implementação do .NET Core pode ser substituída por uma implementação mais madura. Assim, você pode considerar aproveitar a estrutura da DI do .NET Core como um “componente” através do qual pode conectar outras estruturas da DI quando surgir uma necessidade no futuro. Assim, você não terá que definir seu próprio componente DI “personalizado”, mas poderá aproveitar o do .NET Core como um padrão para o qual qualquer cliente/aplicativo pode conectar uma implementação personalizada.

Algo a notar sobre o ASP.NET Core é que ele aproveita completamente a DI. Sem dúvida alguma, será uma ótima prática se você precisar e é especialmente importante ao tentar substituir as implementações fictícias de uma biblioteca em seus testes de unidade. A desvantagem é que, em vez de uma chamada simples para um construtor com o novo operador, são necessárias a complexidade do registro da DI e as chamadas GetService. Não posso deixar de imaginar se, talvez, a linguagem C# poderia simplificar isso, mas, com base no design atual do C# 7.0, isso não acontecerá em um futuro próximo.


Mark Michaelis é fundador da IntelliTect, onde atua como arquiteto técnico principal e instrutor. Há quase 20 anos trabalha como Microsoft MVP, e é Diretor Regional da Microsoft desde 2007. Michaelis atua em diversas equipes de análise de design de software da Microsoft, incluindo C#, Microsoft Azure, SharePoint e Visual Studio ALM. Ele dá palestras em conferências de desenvolvedores e escreveu diversos livros, incluindo o mais recente, “Essential C# 6.0 (5ª Edição)” (itl.tc/­EssentialCSharp). Você pode contatá-lo pelo Facebook, em facebook.com/Mark.Michaelis, pelo seu blog IntelliTect.com/Mark, no Twitter: @markmichaelis ou pelo email mark@IntelliTect.com.

Agradecemos aos seguintes especialistas técnicos da IntelliTect pela revisão deste artigo: Kelly Adams, Kevin Bost, Ian Davis e Phil Spokas