Search code examples
c#asp.net-core.net-coremediatr

How to register generic type mediatR handler?


I am trying to use a Generic type mediatR IRequestHandler in my application. But I get an error "No service registered". Here is the sample application to re-create the error.

This is my Program.cs file which has to minimal endpoints GetAdultUsers and GetGmailUsers. The purpose of these endpoints is to mimic the use case where i can use two different specifications (AKA Specification Pattern) with same handler. One returns a List of type UserDto and other one GmailDto.

using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();


string connectionString = builder.Configuration.GetConnectionString("DefaultConnection") 
                            ?? throw new InvalidOperationException("Connection string is missing");

builder.Services.AddDbContext<IApplicationDbContext, AppDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/Users/Adults", ([FromServices] IMediator mediator) =>
{
    return mediator.Send(new GetUsersQuery<UserDto>(new AdultSpecification()));
}).Produces<List<UserDto>>()
.WithName("GetAdultUsers")
.WithOpenApi();

app.MapGet("/Users/GmailAccounts", ([FromServices] IMediator mediator) =>
{
    return mediator.Send(new GetUsersQuery<GmailDto>(new GmailAccountSpecification()));
}).Produces<List<GmailDto>>()
.WithName("GetGmailUsers")
.WithOpenApi();

app.Run();

GetUsersQuery.cs

using Ardalis.Specification;
using MediatR;

public class GetUsersQuery<T> : IRequest<List<T>> where T : class
{
    public ISpecification<User, T> Specification { get; }

    public GetUsersQuery(ISpecification<User, T> specification)
    {
        Specification = specification;
    }
}

GetUsersQueryHandler.cs

using Ardalis.Specification.EntityFrameworkCore;
using MediatR;
using Microsoft.EntityFrameworkCore;

public class GetUsersQueryHandler<T> : IRequestHandler<GetUsersQuery<T>, List<T>> where T : class
{
    private readonly IApplicationDbContext _context;

    public GetUsersQueryHandler(IApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<List<T>> Handle(GetUsersQuery<T> request, CancellationToken cancellationToken)
    {
        return await _context.Users
            .WithSpecification(request.Specification).ToListAsync(cancellationToken: cancellationToken);
    }
}

Specifiations/AductSpec

using Ardalis.Specification;

public class AdultSpecification : Specification<User, UserDto>
{
    public AdultSpecification()
    {
        Query.Select(x => new UserDto(x.Username, x.Email));
        Query.Where(x => x.Age >= 18);
    }
}

public record UserDto(string Username, string Email);


Specifiations/GmailSpec

using Ardalis.Specification;

public class GmailAccountSpecification : Specification<User, GmailDto>
{
    public GmailAccountSpecification()
    {
        Query.Where(x => x.Email.EndsWith("@gmail.com"));
    }
}

public record GmailDto(string Username, string Email);

Here is my User model

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public int Age { get; set; }
}

I am using Postgres Db with EFcore: AppDbContext.cs

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext, IApplicationDbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
    {
        return base.SaveChangesAsync(cancellationToken);
    }
}

public interface IApplicationDbContext
{
    DbSet<User> Users { get; set; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

Here is the error message:

Exception has occurred: `CLR/System.InvalidOperationException`
An exception of type `System.InvalidOperationException` occurred in Microsoft.Extensions.DependencyInjection.Abstractions.dll but was not handled in user code: `No service for type 'MediatR.IRequestHandler`2[GetUsersQuery`1[UserDto],System.Collections.Generic.List`1[UserDto]]' has been registered.`
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at MediatR.Wrappers.RequestHandlerWrapperImpl`2.<>c__DisplayClass1_0.<Handle>g__Handler|0()
   at MediatR.Wrappers.RequestHandlerWrapperImpl`2.Handle(IRequest`1 request, IServiceProvider serviceProvider, CancellationToken cancellationToken)
   at MediatR.Mediator.Send[TResponse](IRequest`1 request, CancellationToken cancellationToken)
   at Program.<>c.<<Main>$>b__0_2(IMediator mediator) in /home/hardik/code/dotnet/sample-app/src/Program.cs:line 34
   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.<Invoke>d__5.MoveNext()

Solution

  • You need a quite complex map, your generic class GetUsersQueryHandler doesn't have the same arity of the implementation interface IRequestHandler (this means that it has a different number of generic type arguments).

    .NET default DI system does not allow this kind of mapping. It doesn't even allow to provide an implementation factory for an open generic type.

    So the two first solutions that comes to our mind can NOT be adopted:

    1. Generic map:

      builder.Services.AddTransient(typeof(IRequestHandler<,>), typeof(GetUsersQueryHandler<>);
      
    2. Factory pattern:

      builder.Services.AddTransient(typeof(IRequestHandler<,>), sp =>
      {
         // Do something
      });
      

    To achieve what you need we need to use an advanced DI system like Autofac.

    Here's an example of its configuration (reference to NuGet package Autofac.Extensions.DependencyInjection is required):

    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
    builder.Host.ConfigureContainer<ContainerBuilder>(b =>
    {
        b.RegisterGeneric(typeof(GetUsersQueryHandler<>))
           .AsImplementedInterfaces() // Same as As(typeof(IRequestHandler<,>)
           .InstancePerDependency(); // <- Add as transient (same lifetime of MediatR services)
    });