Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IMediatR - Generic Request Handler for generic Requests

I'm currently facing a problem with IMediatR IRequestHandlers and IRequest. To give a little bit of context, on our tool, the user can request access to the tool by filling a couple of fields. Currently, we have two levels of access, meaning we should have two IRequestHandlers, one for each. Since what we have to do is just register the access request to the database and send an email, I thought that a generic IRequestHandler could be created to handle those situations (and it would help in the future if we create move levels of access).

I've made a little research about how to generically inject those types of handlers, but I could only find non-generic injections. I wonder if there is a way to inject those types of generic IRequestHandlers for those IRequests I'm presenting right bellow?

Here's the code I've created for the IRequest.

public class CreateAccessRequest<TViewModel> : IRequest<OperationResult<long>>, IValidatable
    where TViewModel : AccessRequestViewModel
{
    public TViewModel Request { get; set; }
}

Here's the Request Handler, the more generalized one I could create.

public class CreateAccessRequestHandler<TViewModel, TNotificationModel, TEntity> : IRequestHandler<CreateAccessRequest<TViewModel>, OperationResult<long>>
    where TViewModel : AccessRequestViewModel
    where TEntity : Entity
    where TNotificationModel : INotification
{
    private readonly IRepository<TEntity> _accessRequestRepository;
    private readonly IMediator _mediator;
    private readonly IMapper _mapper;

    public CreateAccessRequestHandler(IRepository<TEntity> accessRequestRepository, IMediator mediator, IMapper mapper)
    {
       // initalization ...
    }

    public async Task<OperationResult<long>> Handle(CreateAccessRequest<TViewModel> request, CancellationToken cancellationToken)
    {
       // mapping, saving to database and sending notifications...
    }
}

And finally, here's the injection code for the handlers

void ConfigureMediatR(Container container)
{
    var assemblies = GetAssemblies().ToArray();
    container.RegisterSingleton<IMediator, Mediator>();
    container.Register(typeof(IRequestHandler<,>), assemblies);
}

I am creating the request just like that:

// CreateApplicationAccessRequestViewModel inherits AccessRequestViewModel
var request = new CreateAccessRequest<CreateApplicationAccessRequestViewModel>();

MediatR didn't find the request handler to the request and fired this exception:

Error constructing handler for the request of type MediatR.IRequestHandler<CreateAccessRequest<CreateApplicationAccessRequestViewModel>, OperationResult<long>>. Register your handlers with the container. See the samples in GitHub for example.

I couldn't get away with just that implementation, I had to create a class that inherits from CreateAccessRequestHandler with the correct generic types, and then it worked:

public sealed class CreateApplicationAccessRequestHandler : CreateAccessRequestHandler<CreateApplicationAccessRequestViewModel, ApplicationAccessRequestNotificationModel, ApplicationAccessRequest>
    {
        public CreateApplicationAccessRequestHandler(IRepository<ApplicationAccessRequest> accessRequestRepository, IMediator mediator, IMapper mapper)
            : base(accessRequestRepository, mediator, mapper)
        { }
    }

My idea is to use only the generic Handler without the need to create derived classes to each of the access request types. Is there a way to do it?

--- EDIT: ---

I used the code that Steven placed in his answer and it almost worked in my case. I'll explain this almost bellow:

var types = container.GetTypesToRegister(typeof(IRequestHandler<,>), assemblies, 
new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true });

container.Register(typeof(IRequestHandler<,>), types.Where(t => !t.IsGenericTypeDefinition));

foreach(var openGenericType in types.Where(t => t.IsGenericTypeDefinition)) {
    container.Register(typeof(IRequestHandler<,>), openGenericType);
}

I registered the generic and non-generic types separately and it works in most situations, except when you have an unresolvable type, which is my case with TEntity.

SimpleInjector doesn't know how to resolve TEntity because (I guess) I didn't supply it to IRequestHandler in the definition.

It would work if I somewhat placed TEntity in the request (i.e. CreateAccessRequest<TViewModel, TEntity>), but it would break the project's architecture. So I'm picking the backup implementation I mentioned here (create concrete handlers out of those generic) to at least keep the code centralized.

like image 520
Vanilson Nogueira Avatar asked Feb 07 '20 18:02

Vanilson Nogueira


1 Answers

MediatR didn't find the request handler to the request

MediatR is just a bunch of interfaces; it is not responsible for finding the request handler—Simple Injector is.

You are currently using the following code to register your handlers:

container.Register(typeof(IRequestHandler<,>), assemblies);

This will register:

all concrete, non-generic, public and internal types in the given set of assemblies that implement the given openGenericServiceType with container's default lifestyle

(As stated by the XML documentation)

What you want to do is register not only the non-generic, but the generic types as well. This can be achieved in multiple ways. For instance by appending those generic types:

container.Register(typeof(IRequestHandler<,>), assemblies);
container.Register(typeof(IRequestHandler<,>), typeof(CreateAccessRequestHandler<,>));

If, however, you have many open-generic implementations, batch registering them might be more effective:

var types =
    container.GetTypesToRegister(typeof(IRequestHandler<,>), assemblies,
        new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true });

container.Register(typeof(IRequestHandler<,>), types);
like image 159
Steven Avatar answered Nov 05 '22 16:11

Steven