Injeção de dependências no .NET Core 2.2
O .NET Core 2.2 possui suporte ao DI (dependecy injection - injeção de dependência), técnica onde um objeto fornece as dependências de outro objeto, visando diminuir o acoplamento entre os módulos de uma aplicação.
A injeção de dependência (DI) é um importante padrão de projeto que implementa o baixo acoplamento entre os diversos módulos de um projeto. Por exemplo, na Figura 1, quando a classe A utiliza funcionalidades da classe B, pode-se dizer que a classe A possui dependência da classe B. A dependência é então qualquer objeto exigido por outro objeto.
A DI realiza a associação representada acima, entre o tipo solicitado pelo cliente (a interface é a mais comum) e o tipo do retorno. Neste caso, não é o cliente que determina o que será instanciado, mas sim a DI que determina o retorno. Desta forma, a DI fornece uma instância do serviço (e não o cliente que instancia diretamente).
Por que utilizar?
Dentre as principais motivações de utilizar os princípios da DI (Dependecy Injection) estão:
- Evitar problemas com multi-threading;
- Evitar potenciais bugs;
- Evitar falha de memória;
- Design de serviços e suas dependências.
Quando instanciamos um objeto no .NET com chamada para o construtor, cria-se uma conexão acoplada do aplicativo com o objeto instanciado. Em alguns serviços, por exemplo Logon (utilizando Log4Net e NLog para clientes diferentes), a dependência pode não funcionar adequadamente, ao referenciar ambos os serviços de logon.
Utilizando DI, o aplicativo solicita uma coleção de serviços para a instância, ao invés de solicitar o serviço diretamente com o operador. Esta solicitação não é de um tipo específico (para evitar acoplamento), mas sim, a uma interface como ILoggerFactory para que o provedor de serviço (Log4Net ou NLog) a implemente.
Na prática
Colocando em prática, vamos criar um projeto MVC com uma definição de interface e uma implementação. Para isso, adicione dois novos itens ao projeto criado:
- Interface – utilize o nome ILab01;
- Classe – utilize o nome Lab01A.
A interface foi criada com a declaração de uma mensagem inicial:
public interface ILab01
{
string MsgInicial();
}
E a classe de implementação com o retorno, referenciando a interface criada anteriormente:
public class Lab01A : ILab01
{
public string MsgInicial()
{
return $"Primeira mensagem {nameof(Lab01A)}";
}
}
Em seguida, registre Lab01A no container do DI, acessando o arquivo Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILab01, Lab01A>();
}
Após o registro da implementação da interface, acesse a pasta “Controllers” (arquivo HomeController.cs) e adicione o construtor da injeção em HomeController : Controller.
public ILab01 Lab01 { get; set; }
public HomeController(ILab01 Lab)
{
Lab01 = Lab;
}
public IActionResult Index()
{
var mensagem = Lab01.MsgInicial();
return Content(mensagem);
}
Como estamos trabalhando com uma interface, o resultado é o item registrado no DI container. Para múltiplas implementações, pode-se utilizar outros recursos para ordenar a exibição dos itens no runtime.
E quais são as vantagens e desvantagens de utilizar injeção de dependências?
Vantagens
- Classes mais modulares, pois dependem apenas da Interface de dependências passadas;
- Facilita o teste em partes isoladas e a reorganização de partes genéricas em novas aplicações;
- Reutilização e manutenção do código;
- Ajuda em teste unitário e a obter menor acoplamento.
Desvantagens
- Desvantagens
- Muitos erros em tempo de compilação são enviados para runtime;
- O uso em excesso pode levar a problemas de gerenciamento, entre outros;
- A implementação com reflexão ou programação dinâmica pode impedir o uso da automação do IDE.
Padrões de DI (dependecy injection)
Constructor Injection
É um padrão de DI utilizado para declarar e obter dependências de um serviço por meio do construtor do serviço. O uso é recomendado quando a dependência for obrigatória, garantindo assim a declaração da dependência na definição da classe.
Assim, a dependência estará pronta para o uso durante todo o ciclo de vida do objeto que a consome. No exemplo abaixo, VendaIngresso injeta IEventoDisponibilidade como dependência no construtor e o utiliza no método Delete.
public class VendaIngresso
{
private readonly IEventoDisponibilidade _eventoDisponibilidade;
public VendaIngresso(IEventoDisponibilidade eventoDisponibilidade)
{
_eventoDisponibilidade = eventoDisponibilidade;
}
public void Delete (int id)
{
_eventoDisponibilidade.Delete(id);
}
}
Boas práticas
- Definir as dependências requeridas de forma explícita no serviço construtor, assim o serviço não pode ser construído sem as dependências;
- Atribuir a injected dependency como somente leitura a fim de evitar atribuições indevidas a ele dentro de um método.
Property Injection
Property injection utiliza propriedades de escrita, ao invés de parâmetros de construtor para executar a injeção. A injeção de métodos define as dependências através do método.
O container de injeção de dependência padrão do ASP.NET Core não possui suporte à injeção de propriedade. Por isso, deve ser utilizado outro container que suporte a injeção de propriedade. No exemplo abaixo, VendaIngresso está declarando uma propriedade Mensagem com setter público. O dependency injection container pode definir Mensagem se ele estiver disponível.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace AppEventos
{
public class VendaIngresso
{
public IMensagem<VendaIngresso> Mensagem { get; set; }
private readonly IEventoDisponibilidade _eventoDisponibilidade;
public VendaIngresso(IEventoDisponibilidade eventoDisponibilidade)
{
_eventoDisponibilidade = eventoDisponibilidade;
Mensagem = NullLogger<VendaIngresso>.Instance;
}
public void Delete(int id)
{
_eventoDisponibilidade.Delete(id);
Mensagem.LogInformation(
$"Evento apagado com o ID = ");
}
}
}
Service Locator
Service Locator é outro padrão para obter dependências. Ele cria uma camada de abstração no processo, e assim, as dependências são solicitadas a partir de um objeto centralizado. No exemplo abaixo, VendaIngresso está injetando IProvedorServico e resolvendo dependências usando-o.
public class VendaIngresso
{
private readonly IEventoDisponibilidade _eventoDisponibilidade;
private readonly IMensagem<VendaIngresso> _mensagem;
public VendaIngresso(IProvedorServico provedorServico)
{
_eventoDisponibilidade = provedorServico
.GetRequiredService<IEventoDisponibilidade>();
_mensagem = provedorServico
.GetService<IMensagem<VendaIngresso>>() ??
NullLogger<VendaIngresso>.Instance;
}
public void Delete(int id)
{
_eventoDisponibilidade.Delete(id);
_mensagem.LogInformation($"Evento apagado com o ID = ");
}
}
Service Life Times
Há três service lifetimes no ASP.NET Core DI:
- Transient: serviços são criados toda vez que há injeção ou requisição. É altamente recomendado por não precisar se preocupar com multi-threading e falhas de memória;
- Scope: a criação por escopo cria um novo escopo de serviço separado a cada requisição web. Não é recomendado o uso do serviço em aplicações que não sejam web.
- Singleton: serviços criados através do DI container. Geralmente é criado uma única vez para o ciclo de vida completo da aplicação. O uso deve considerar multi-threading e prevenir falhas de memória.
No exemplo utilizando Transient, instanciamos uma nova implementação IServico para cada chamada, aproveitando a injeção automática do construtor.
container.Register<IServico, Servico>(Lifestyle.Transient);
ou
container.Register<IServico, Servico>();
</IServico>
Ou iniciando uma nova instância Servico a cada chamada, utilizado delegate:
container.Register<IServico>(() => new Servico(new SqlRepository()),
Lifestyle.Transient);
Em Scope, após criar o scoped lifestyle padrão, as demais configurações podem acessar este lifestyle através de Lifestyle.Scoped:
var container = new Container();
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
container.Register<IUserContext, AspNetUserContext>(Lifestyle.Scoped);
container.Register<MyAppUnitOfWork>(() => new MyAppUnitOfWork("constr"),
Lifestyle.Scoped);
container.RegisterInstance<IServico>(servico);
Já o Singleton pode ser registrado especificando o tipo do serviço e a implementação como argumentos de tipo genérico. Também pode utilizar o método RegisterInstance
ccontainer.Register<IServico, Servico>(Lifestyle.Singleton);
ou
var servico = new Servico(new SqlRepository());
container.RegisterInstance<IServico>(servico);
Veja que com os princípios apresentados conseguimos criar aplicações altamente desacopladas para incrementar a reusabilidade dos componentes de seus projetos.
Confira também
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
DevCast