Article Image
Article Image
read

Recentemente, passei por um problema ao implementar um middleware que precisava ir até a base de dados em algum momento, em uma aplicação que fazia uso do Entity Framework.

A única dependência direta do middleware era a classe de serviço com as regras de negócio, que por sua vez utilizava o repositório que fazia uso do EF.

Toda vez que eu executava a aplicação eu recebia a seguinte mensagem de erro:

InvalidOperationException: Cannot resolve scoped service ‘MyApplication.MyDbContext’ from root provider.

Como resolver isso?

Para resolver precisamos entender um pouco sobre como o mecanismo de injeção de dependências do NET Core funciona, quando injetamos nossas depêndencias, normalmente, injetamos classes com regras de negócio como Transient, mas, o DBContext vai como Scoped. Mas, o que isso quer dizer? Vamos a uma breve explicação sobre como podemos injetar nossas dependências:

  • Transient - Uma instância é criada para cada vez que o uso da dependência se faz necessário.
  • Scoped - Uma instânia é criada por requisição.
  • Singleton - A instâcia é uma só para qualquer requisição.

Um middleware naturalmente é um singleton, e a injeção de DBContext é feita como scoped, e as classes de serviço contendo as regras de negócio normalmente são injetadas como transient… No momento de realizar a injeção da instância da classe de serviço, que é do tipo transient, no middleware o injetor simplesmente não consegue se resolver e trazer junto de si suas dependências de outro tipo, no caso scoped.

Devemos simplesmente mudar a injeção da classe de serviço para scoped?

A resposta é um categórico não.

Elas devem ser transient, devem ter uma instância para cada uso.

Então como resolvemos o problema?

Simples, o middleware recebe por parâmetro o contexto e o contexto contém o ServiceProvider com todas as injeções que foram feitas, então, precisamos simplesmente resolver a injeção no middleware no momento da execução.

Escrevi um exemplo simples do problema e da solução. Criei uma API com a boa e velha WeatherForecastController e uma estrutura de dados simples só para logar as requisições, sei que pode não ser o melhor caminho para um log de requisições, mas, o fim aqui é didático.

Para essa estrutura usei Docker e MySQL. Já falei um pouco sobre Docker e como usar o MySQL no Docker no artigo “Logando queries do Entity Framework com Serilog”., então, para entender um pouco mais sobre como fazer isso é só dar uma olhada lá.

A estrutura que criei foi a seguinte:

create schema log;

use log;

create table request(
    id int not null primary key auto_increment,
    `request_date` datetime not null,
    `request_path` varchar(500)
);

Depois criei uma classe simples representando essa tabela:

public class Log
{
    public int Id { get; set; }

    public DateTime RequestDate { get; set; }

    public string RequestPath { get; set; }
}

Então configurei o LogDbContext:

public class LogDbContext : DbContext
{
    public DbSet<Log> Logs { get; set; }

    public LogDbContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //Microsoft.EntityFrameworkCore.Relational
        modelBuilder.HasDefaultSchema("log");

        //Microsoft.EntityFrameworkCore.Relational
        modelBuilder.Entity<Log>().ToTable("request");

        modelBuilder.Entity<Log>().HasKey("Id");

        //Microsoft.EntityFrameworkCore.Relational
        modelBuilder.Entity<Log>().Property("RequestDate").HasColumnName("request_date");
        modelBuilder.Entity<Log>().Property("RequestPath").HasColumnName("request_path");
    }
}

Escrevi uma classe simples para servir de ponte com o banco e uma classe fazendo o papel de uma classe com as regras de negócio da aplicação, só para emularmos uma situação mais próxima do mundo real.

LogRepository:

public class LogRepository
{
    private readonly LogDbContext _logDbContext;

    public LogRepository(LogDbContext logDbContext)
    {
        _logDbContext = logDbContext;
    }

    public async Task Insert(Log log)
    {
        _logDbContext.Logs.Add(log);

        await _logDbContext.SaveChangesAsync();
    }
}

LogService:

public class LogService
{
    private readonly LogRepository _logRepository;

    public LogService(LogRepository logRepository)
    {
        _logRepository = logRepository;
    }

    public async Task LogAsync(Log log)
    {
        await _logRepository.Insert(log);
    }
}

E finalmente escrevi o middleware:

public class LogMiddleware
{
    private readonly RequestDelegate _next;

    private LogService _logService;
    
    public LogMiddleware(RequestDelegate next, LogService logService)
    {
        _next = next;

        _logService = logService;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var log = new Log 
        { 
            RequestDate = DateTime.Now, 
            RequestPath = context.Request.Path             
        };

        await _logService.LogAsync(log);

        // Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

E amarrei as coisas no Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient(typeof(LogRepository));
    services.AddTransient(typeof(LogService));

    //Pomelo.EntityFrameworkCore.MySql
    services.AddDbContext<LogDbContext>(options => options.UseMySql("Server=localhost;Database=log;Uid=root;Pwd=password;"), ServiceLifetime.Scoped);

    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...

    app.UseMiddleware<LogMiddleware>();

    //...
}

Notem que fiz uso do mecanismo de injeção de dependência do NET CORE normalmente, como em qualquer outra aplicação. Como trata-se de um exemplo não vi necessidade de criar interfaces para cada dependência, o resultado aqui será o mesmo.

Agora basta executarmos a aplicação. E…

Nosso erro…

E a solução é simples, basta deixarmos de injetar o LogService e resolver internamente. O método InvokeAsync que é executado quando qualquer requisição é feita recebe por parâmetro um HttpContext, esse objeto por sua vez contém uma propriedade chamada RequestServices, que nada mais é que o ServiceProvider e carrega todas as dependências injetadas na aplicação:

public class LogMiddleware
{
    private readonly RequestDelegate _next;

    private LogService _logService;

    //Isso dá erro, não faça assim
    //public LogMiddleware(RequestDelegate next, LogService logService)
    //{
    //    _next = next;

    //    _logService = logService;
    //}

    //Mudamos para a injeção padrão, recebendo somente o RequestDelegate
    public LogMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //Aqui é o pulo do gato, passamos a resolver a dependência na chamada
        //usando o GetService do ServiceProvider
        _logService = (LogService)context.RequestServices.GetService(typeof(LogService));

        var log = new Log 
        { 
        RequestDate = DateTime.Now, 
        RequestPath = context.Request.Path             
        };

        await _logService.LogAsync(log);

        // Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

Agora quando executarmos a aplicação:

E olhando os registros da tabela request no MySQL:

Todo o exemplo está no meu git

Espero que esse artigo ajude de alguma forma e até a próxima!

Esse post foi útil pra você?

Blog Logo

Luiz Faria


Published

Image

Another Developer Blog

Mais um blog de um desenvolvedor...

Back to Overview