Search code examples
c#dependency-injectionhandlermediatormediatr

C# MediatR error: Register your handlers with the container


Every time that i try to call Send from MediatR to any Query/Command that i have, it returns this Exception:

System.InvalidOperationException: 'Error constructing handler for request of type MediatR.IRequestHandler2[CQRSHost.Recursos.Queries.GetTodosProdutosQuery,System.Collections.Generic.IEnumerable1[CQRSHost.Models.Produto]]. Register your handlers with the container. See the samples in GitHub for examples.'

Inner Exception:

InvalidOperationException: Cannot resolve 'MediatR.IRequestHandler2[CQRSHost.Recursos.Queries.GetTodosProdutosQuery,System.Collections.Generic.IEnumerable1[CQRSHost.Models.Produto]]' from root provider because it requires scoped service 'CQRSHost.Context.AppDbContext'.

But i have the AppDbContext in my DI container:

public static IHostBuilder CreateHostBuilder(string[] args)
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

        return Host.CreateDefaultBuilder(args)
            .UseSerilog()
            .UseEnvironment("Development")
            .ConfigureHostConfiguration(hostConfig =>
            {
                hostConfig.SetBasePath(Directory.GetCurrentDirectory());
                hostConfig.AddEnvironmentVariables("DSO_");
            })
            .ConfigureServices((context, services) =>
            {
                services.AddSingleton(ConfigureLogger());
                services.AddDbContext<AppDbContext>(options =>
                options.UseSqlServer(
                    "Server=localhost;Database=newdatabase;User Id=sa;Password=P@ssw0rd!@#$%;",
                    b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));

                services.AddHostedService<NewService>();

                //services.AddMediatR(Assembly.GetExecutingAssembly());
                //services.AddMediatR(typeof(GetTodosProdutosQuery).GetTypeInfo().Assembly);
                services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies());
            });
    }

Here is the service that i use to call the query:

public class NewService : IHostedService
{
    private readonly ILogger _logger;
    private readonly IMediator _mediator;

    public NewService(ILogger logger, IMediator mediator)
    {
        _logger = logger;
        _mediator = mediator;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var command = new GetTodosProdutosQuery();
        var response = await _mediator.Send(command);

        _logger.Information($"First Name: {response.First()?.Nome}");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

And here is what my project looks like: ProjectImage

The commented lines is what i've already tryied to solve.

What am i doing wrong?


Solution

  • The exception "Register your handlers with the container." is misleading. The real error is described in the inner exception:

    Cannot resolve 'MediatR.IRequestHandler<GetTodosProdutosQuery, IEnumerable>' from root provider because it requires scoped service 'CQRSHost.Context.AppDbContext'.

    This happens because you inject the IMediator into a singleton consumer NewService. The Mediator implementation depends on a IServiceProvider but as NewService is singleton, it is resolved from the root container, and so will all its dependencies recursively. This means that once Mediator starts resolving from its IServiceProvider, it also resolves from the root container. And scoped services can't be resolved from the root container, because that would lead to bugs, because that scoped service would be cached for the lifetime of the root container, and reused for the lifetime of the root container - which means indefinitely.

    The solution is to inject an IServiceScope into NewService create a scope from within its StartAsync and resolve the IMediator from there:

    public class NewService : IHostedService
    {
        private readonly IServiceProvider _container;
    
        public NewService(IServiceProvider container)
        {
            _container = container;
        }
    
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            await using (var scope = _container.CreateScope())
            {
                var logger = scope.ServiceProvider.GetRequiredService<ILogger>();
                var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
    
                var command = new GetTodosProdutosQuery();
                var response = await mediator.Send(command);
    
                logger.Information($"First Name: {response.First()?.Nome}");
            }
        }
    
        ...
    }
    

    Another, perhaps more convenient option would be to ensure that the mediator always resolves from a new scope. This can be achieved using the following code:

    public record ScopedSender<TSender>(IServiceProvider Provider)
        : ISender where TSender : ISender
    {
        public Task<TResponse> Send<TResponse>(
            IRequest<TResponse> request, CancellationToken ct)
        {
            async using (var scope = Provider.CreateScope());
            var sender = scope.ServiceProvider.GetRequiredService<TSender>();
            return await sender.Send(request, ct);
        }
        
        public Task<object?> Send(object request, CancellationToken ct)
        {
            async using (var scope = Provider.CreateScope());
            var sender = scope.ServiceProvider.GetRequiredService<TSender>();
            return await sender.Send(request, ct);
        }
        
        public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
            IStreamRequest<TResponse> request, CancellationToken ct)
        {
            async using (var scope = Provider.CreateScope());
            var sender = scope.ServiceProvider.GetRequiredService<TSender>();
            return await sender.CreateStream(request, ct);
        }
        
        public IAsyncEnumerable<object?> CreateStream(object request, CancellationToken ct)
        {
            async using (var scope = Provider.CreateScope());
            var sender = scope.ServiceProvider.GetRequiredService<TSender>();
            return await sender.CreateStream(request, ct);
        }
    }
    

    Now configure this as follows:

    services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies());
    services.AddTransient<Mediator>();
    services.AddSingleton<ISender, ScopedSender<Mediator>>();
    

    Now you can safely inject your ISender into yout NewService without having to apply scoping:

    public class NewService : IHostedService
    {
        private readonly ILogger _logger;
        private readonly IMediator _sender;
    
        public NewService(ILogger logger, ISender sender)
        {
            _logger = logger;
            _sender = sender;
        }
    
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            var command = new GetTodosProdutosQuery();
            var response = await _sender.Send(command);
    
            _logger.Information($"First Name: {response.First()?.Nome}");
        }
    
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }