Injeção de Dependência com .NET 5

Neste artigo eu falo sobre Injeção de Dependência em .NET. Vou falar sobre seu conceito e benefícios, os tipos de ciclos de vida dos objetos ao utilizar injeção de dependência com .NET, e como utilizar esse padrão.

Repositório no Github: https://github.com/luisdeol/dependency-injection-article


O que é Injeção de Dependência

É uma técnica que permite fornecer instâncias de dependências de uma classe, ao invés de que a classe que depende delas as construa por si mesmo, através da palavra-chave new por exemplo.

É de suma importância para processos de refatoração, como por exemplo no seguinte fluxo:

  • Código que envia um e-mail é extraído para uma classe específica para tratar disso
  • A classe que dependia dele passa a depender da classe criada. De início a responsabilidade de instanciar poderia ficar com a classe anterior, mas ela seguiria com responsabilidades que não são dela, como de passar configuração para o construtor da classe de e-mails;
  • Ao invés disso, a instância da classe de e-mail é passada pelo construtor, livrando a classe de responsabilidade de inicializar.
  • Como resultado, o código passa a apresentar melhor separação de responsabilidades e também melhor legibilidade. Para completar a refatoração, o ideal seria passar a utilizar uma interface da classe responsável pelos e-mails, atendendo ao princípio SOLID de Dependency Inversion e melhorando a testabilidade. Mas isso é um tema para o próximo artigo!

O .NET Core/.NET tem uma funcionalidade de injeção de dependência nativa dele, não precisando mais adicionar pacotes externos como se utilizava com o .NET Framework (como o Ninject).


Tempo de vida

Um conceito importante de se entender a respeito de injeção de dependência é o de tipos de tempo de vida das instâncias. Eles se diferenciam de acordo com o quão duradouro é seu estado.

  • Transient: tempo de vida mais curto dos três, com a instância sendo criada e utilizada apenas no contexto em que está. Por exemplo, se você utilizar uma instância de configuração com tempo Transient em diferentes classes e alterá-la em seu contexto, não seria replicado para as outras, pois cada contexto seria sua própria instância.
  • Scoped: já este tempo de vida define que a instância é a mesma para a requisição, podendo variar os contextos (classes/construtores) em que é utilizada a mesma instância. Quando você utiliza um DbContext do Entity Framework Core e configura com AddDbContext na classe Startup, a instância que você utiliza do DbContext tem o tempo de vida Scoped. Ou seja, operações são realizadas no mesmo contexto de dados durante a mesma requisição, permitindo que caso implemente o padrão Unit of Work você apenas tenha o método SaveChanges, sem necessidade de controlar o estado dos Repositórios internamente já que usarão a mesma instância do DbContext para a requisição.
  • Singleton: instâncias com este tempo de vida duram pelo tempo de vida da aplicação. O nome é o mesmo que do Singleton pattern, já dando uma dica de seu tempo de vida. Geralmente utilizada para configurações e outras informações que dificilmente mudariam.

Como utilizar com ASP.NET Core

Para começar a utilizar, é essencial citar um erro bem comum ao se utilizar injeção de dependência, que é de adicionar uma instância no construtor de uma classe, e esquecer de configurar esta instância na classe Startup. Acredito que todos desenvolvedores já esqueceram alguma vez, e na hora de executar a aplicação e acessar um endpoint, por exemplo, o erro é lançado de que o .NET não consegue resolver a dependência requisitada.

O exemplo de utilização é bem simples: vou criar uma classe chamada EmailConfiguration, e uma classe chamada EmailService. Dentro de EmailService vou ter um método que se chama Send, e que vai apenas imprimir no Console o valor da propriedade From e Subject da classe EmailConfiguration. Vou alterar o valor de From e Subject no Controller, e poderemos ver com clareza se no EmailService o valor já está alterado ou não, e também se entre requisições essa alteração persiste.

Logo abaixo você pode ver a implementação simples das duas classes.

using System;

namespace InjecaoDependenciaArtigo.API.Models
{
    public class EmailConfiguration
    {
        public EmailConfiguration()
        {
            From = "default@mail.com";
            Subject = "Default Subject";
        }

        public string From { get; set; }
        public string Subject { get; set; }

        public void Print() {
            Console.WriteLine($"From: {From}, Subject: {Subject}");
        }
    }
}

Essa classe tem valores iniciais para From e Subject, e os testes que faremos são exatamente para validar se são alterados quando utilizados em EmailService ou não.

namespace InjecaoDependenciaArtigo.API.Models
{
    public class EmailService
    {
        private readonly EmailConfiguration _configuration;
        public EmailService(EmailConfiguration configuration)
        {
            _configuration = configuration;
        }

        public void Send() {
            _configuration.Print();
        }
    }
}

A classe EmailService tem apenas o método Send, e ele chamada o método Print para exibir os dados de EmailConfiguration, que está sendo inserida aqui por injeção de dependência.

Com as duas classes configuradas, vamos configurar na classe Startup, mais especificamente no método ConfigureServices, como no código abaixo. Utilizamos os métodos AddTransient e AddScoped para isso, se referindo cada um ao seu tempo de vida especificado. O foco aqui é no EmailConfiguration, então deixarei o EmailService como Scoped.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<EmailConfiguration>();
            services.AddScoped<EmailService>();
            
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "InjecaoDependenciaArtigo.API", Version = "v1" });
            });
        }

Finalmente, criei um Controller chamado EmailsController, e lá utilizo estas instâncias via injeção de dependência no construtor, altero os valores de EmailConfiguration no endpoint de GET e chamo Send do EmailService para verificar se a mudança foi persistida entre instâncias na mesma requisição. Antes de se alterar esses valores eu chamo o método Print também para verificar se o estado foi alterado entre requisições.

using InjecaoDependenciaArtigo.API.Models;
using Microsoft.AspNetCore.Mvc;

namespace InjecaoDependenciaArtigo.API.Controllers
{
    [Route("api/[controller]")]
    public class EmailsController : ControllerBase
    {
        private readonly EmailConfiguration _configuration;
        private readonly EmailService _emailService;
        public EmailsController(EmailConfiguration configuration, EmailService emailService)
        {
            _configuration = configuration;
            _emailService = emailService;

            _configuration.Print();
        }

        [HttpGet]
        public IActionResult Get(){
            _configuration.From = "luisdev@email.com";
            _configuration.Subject = "Welcome!";

            _emailService.Send();

            return Ok(_configuration);
        }
    }
}

Ao se executar este endpoint (executar a aplicação e fazer uma requisição para api/emails), temos o resultado abaixo como saída no terminal. Confirmamos que com o tempo de vida Transient o estado de EmailConfiguration não é mantido entre contextos EmailsController e EmailService.

From: default@mail.com, Subject: Default Subject
From: default@mail.com, Subject: Default Subject

Alteramos para Scoped lá na classe Startup, como no código abaixo.

services.AddScoped<EmailConfiguration>();

Ao executar novamente, obtemos o resultado abaixo, mostrando que a alteração da instância foi mantida e acessada no EmailService.

From: default@mail.com, Subject: Default Subject
From: luisdev@email.com, Subject: Welcome!

Mas se executarmos novamente, veremos que o primeiro print segue mostrando o valor inicial da instância da classe, não persistindo a alteração entre requisições.

Finalmente, alteramos a configuração para utilizar o tempo de vida Singleton.

services.AddSingleton<EmailConfiguration>();

E abaixo confirmamos que, mesmo entre requisições, o primeiro print mostra o valor já alterado na requisição anterior. O primeiro bloco é resultado da primeira execução, e o segundo é da segunda.

From: default@mail.com, Subject: Default Subject
From: luisdev@email.com, Subject: Welcome!
From: luisdev@email.com, Subject: Welcome!
From: luisdev@email.com, Subject: Welcome!

O segundo bloco mostra que a alteração foi persistida entre requisições, já que o método Print da primeira linha é chamado ANTES da alteração no endpoint. E com isso, conseguimos validar e testar cada um dos tempos de vida de Injeção de Dependência do .NET!


Inscreva-se na lista de espera do Método .NET Direto ao Ponto, um treinamento completo sobre C#, APIs com ASP.NET Core e Microsserviços:  Inscreva-se aqui.

São quase 200 vídeo-aulas sobre temas como C#, ASP NET Core 5, EF Core, CQRS, Clean Architecture, Autenticação e autorização com JWT, Testes Unitários, além de mini-cursos em Microsserviços, Performance em .NET, ASP NET Core e Azure, Docker, Carreira Internacional em .NET, e mais.


Neste artigo expliquei o que é Injeção de Dependência, seus benefícios, e como ela funciona em .NET, explicando e exemplificando os tempos de vida de uma instância na injeção de dependência. Espero que tenha ficado claro, e até o próximo artigo!