Injeção de dependência no ASP.NET Core

A injeção de dependência (DI) é um padrão fundamental de desenvolvimento usado para criar sistemas desacoplados, modularizados e mais fáceis de testar. No contexto do ASP.NET Core, a injeção de dependência é uma técnica essencial que facilita a gestão de objetos e suas dependências. Vamos explorar como funciona esse conceito e como aplicá-lo na prática.

O que é injeção de dependência?

De maneira simples, injeção de dependência é uma técnica que ajuda a “dar” para um objeto aquilo de que ele precisa, sem que esse objeto precise criar ou saber como obter o que ele necessita. Isso ajuda a reduzir o acoplamento, ou seja, diminui a dependência direta entre os objetos, tornando o código mais fácil de modificar e testar. Em vez de uma classe criar diretamente suas instâncias (por exemplo, utilizando a palavra-chave new), essas instâncias são “injetadas” nela de fora, normalmente através de um construtor, método ou propriedade. Isso ajuda a reduzir o acoplamento entre as classes e melhora a testabilidade do código.

O problema do acoplamento forte

Vamos ver um exemplo para entender o problema. Imagine que temos uma classe “Cliente” que precisa da classe “Pedido”. Em vez de simplesmente pedir ao sistema que a classe “Pedido” seja criada, a classe “Cliente” vai lá e cria o objeto “Pedido” diretamente, assim:

				
					public class Cliente
{
    public void ObterPedidos()
    {
        Pedido pedido = new Pedido(); 
        var pedidos = pedido.GetPedidos();
    }
}

				
			

Nesse código, a classe “Cliente” está fortemente acoplada à classe “Pedido”. Ou seja, a “Cliente” depende totalmente da “Pedido” e, se a classe “Pedido” mudar, a “Cliente” vai precisar ser alterada também. Isso torna o código mais difícil de manter e testar.

Violação do princípio da responsabilidade única (SRP)

A classe “Cliente” não deveria ser responsável por criar a instância de “Pedido”. Isso é uma violação do Princípio da Responsabilidade Única (SRP), que diz que uma classe deve ter apenas uma responsabilidade. A classe “Cliente” deveria apenas “pedir” os pedidos, não se preocupar em como criar o “Pedido”.

A solução: injeção de dependência

A solução para esse problema é usar a injeção de dependência. Em vez de a classe “Cliente” criar o “Pedido”, vamos delegar essa tarefa para outra classe. Ou seja, vamos pedir para que o sistema nos forneça o “Pedido” sempre que precisarmos dele.

Primeiro, criamos uma interface “IPedido”, que define o que a classe “Pedido” deve fazer (no nosso caso, retornar os pedidos).

				
					public interface IPedido
{
    List<string> GetPedidos();
}

				
			

Depois, criamos a classe “Pedido” que vai implementar essa interface. A classe “Pedido” é responsável por retornar os pedidos.

				
					public class Pedido : IPedido
{
    public List<string> GetPedidos()
    {
        return new List<string> { "XTudo", "XSalada" };
    }
}

				
			

Agora, a classe “Cliente” não cria mais o “Pedido” diretamente. Em vez disso, ela vai receber um objeto do tipo “IPedido” e usar ele.

				
					public class Cliente
{
    private readonly IPedido _pedido;

    public Cliente(IPedido pedido)
    {
        _pedido = pedido;
    }

    public void ObterPedidos()
    {
        var pedidos = _pedido.GetPedidos();
        foreach (var pedido in pedidos)
        {
            Console.WriteLine(pedido);
        }
    }
}

				
			

No código acima a palavra-chave “readonly” garante que o campo “_pedido” só possa ser atribuído no momento da construção da classe e que sua referência não possa ser alterada após a criação da instância.

O construtor da classe recebe a instância de “IPedido” via injeção de dependência. Isso significa que, ao criar uma instância de “Cliente”, o sistema cuida de passar a instância de “Pedido”, e a classe “Cliente” usa essa dependência sem precisar saber como o “Pedido” foi criado.

Configuração no ASP.NET Core

Agora que fizemos a classe “Cliente” depender de uma abstração (a interface IPedido), precisamos configurar o container de injeção de dependência do ASP.NET Core para fornecer a instância de “Pedido” sempre que a classe “Cliente” precisar dela.

No arquivo “Program.cs”, registramos a interface “IPedido” e sua implementação concreta, “Pedido”:

				
					public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Registra a interface IPedido e a classe Pedido no container DI
        builder.Services.AddScoped<IPedido, Pedido>();
        
        // Registra a classe Cliente que depende de IPedido
        builder.Services.AddScoped<Cliente>();

        var app = builder.Build();

        app.Run();
    }
}

				
			

Aqui, usamos “AddScoped” para registrar que sempre que precisarmos de um “IPedido”, o sistema deve nos fornecer uma instância de “Pedido”. O ASP.NET Core vai automaticamente “injetar” o objeto “Pedido” na classe “Cliente”.

Tipos de registros no container de injeção de dependência

No ASP.NET Core, o container de injeção de dependência (DI) gerencia as dependências da aplicação. Quando registramos os serviços, podemos especificar o tempo de vida desses serviços, ou seja, por quanto tempo eles serão mantidos na memória. O ASP.NET Core oferece três tipos principais de tempo de vida para os serviços:

  1. Scoped
  2. Singleton
  3. Transient

Cada tipo de registro tem suas características e é usado em cenários específicos. Vamos entender cada um deles com exemplos práticos.

Scoped (Escopo)

Os serviços registrados como Scoped são criados uma vez por requisição (ou escopo). Ou seja, toda vez que uma nova requisição HTTP chega à aplicação, o container DI cria uma nova instância desse serviço. Esse tipo de serviço é útil quando queremos manter o estado durante o processamento de uma requisição, mas não precisamos compartilhar essa instância entre diferentes requisições.

Use o “Scoped” para serviços que devem ser usados por toda a duração de uma única requisição HTTP.

Um exemplo de utilização é em repositórios de banco de dados ou serviços que necessitam manter o estado durante a execução de uma requisição.

No “Program.cs”, registramos o serviço como Scoped:

				
					builder.Services.AddScoped<IOrderService, OrderService>();
				
			

Toda vez que uma requisição HTTP chega, o container cria uma instância de “OrderService”. No entanto, essa instância será usada apenas durante essa requisição e descartada ao final da mesma.

Singleton (Único)

Os serviços registrados como Singleton são criados uma única vez e compartilhados por toda a vida útil da aplicação. Ou seja, o mesmo objeto será usado durante toda a execução do aplicativo, independentemente do número de requisições ou chamadas ao serviço.

Use Singleton para serviços que não mantêm estado entre requisições e não precisam ser recriados, como serviços de log, configurações de aplicativo ou gerenciadores de conexões.

Ideal para objetos pesados que não precisam ser recriados a cada requisição.

No “Program.cs”, registramos o serviço como Singleton:

				
					 builder.Services.AddSingleton<ILoggerService, LoggerService>();
				
			

Neste caso, o “LoggerService” será instanciado apenas uma vez durante a vida do aplicativo e será reutilizado em todas as requisições. Isso é ideal para serviços que não mantêm estado ou dados específicos de cada requisição.

Transient (Transiente)

Por fim, os serviços registrados como Transient são criados toda vez que são solicitados. Isso significa que cada vez que o serviço for injetado ou chamado, uma nova instância será criada. Esse tipo de serviço é útil quando o estado de um serviço não deve ser compartilhado entre diferentes partes do código e a criação do serviço não é cara.

Use Transient para serviços sem estado que não mantêm dados entre requisições e não exigem a reutilização da mesma instância em várias partes do sistema.

Use em pequenos serviços auxiliares que não devem ser compartilhados entre diferentes componentes.

No “Program.cs”, registramos o serviço como Transient:

				
					  builder.Services.AddTransient<INotificationService, NotificationService>();
				
			

Agora, toda vez que o serviço “INotificationService” for solicitado, uma nova instância de NotificationService será criada. Esse comportamento é ideal quando o serviço não precisa manter estado e a criação de instâncias não é custosa.

Acelere a sua carreira conosco!

Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET StartClique aqui

Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET ExpertClique aqui

Conclusão

Ao adotar a DI, você reduz o acoplamento entre os componentes do sistema, facilita a manutenção do código, garantindo que ele permaneça limpo e de fácil entendimento ao longo do tempo. Com a abordagem correta no uso da DI, você pode criar sistemas mais modulares, organizados e prontos para mudanças, o que resulta em maior agilidade no desenvolvimento e maior qualidade no produto final.