jbogard / MediatR.Extensions.Microsoft.DependencyInjection

MediatR extensions for Microsoft.Extensions.DependencyInjection
MIT License
327 stars 89 forks source link

Generic Request and Handler With AspNet Core DI #96

Open vulegend opened 3 years ago

vulegend commented 3 years ago

I have an issue with registering a request handler with generic request. I've searched everywhere, old issues, stack overflow and i couldn't find a single working answer. I've also tried everything suggested in any similar thread on this topic and still no luck.

I am using AspNet Core DI and MediatR version 9.0. Here is a sample of my request :

 public record GetSearchItemsQuery<TIndex> : MediatR.IRequest<RawEsSearchResponse<TIndex>> where TIndex : SmartSearchableIndex
    {
        public SmartQuery SmartQuery { get; init; }
        public QueryParameters Parameters { get; init; }
        public Func<SourceFilterDescriptor<TIndex>, ISourceFilter> IncludedFields { get; init; }

        public GetSearchItemsQuery(SmartQuery smartQuery, QueryParameters parameters, Func<SourceFilterDescriptor<TIndex>, ISourceFilter> includedFields)
        {
            SmartQuery = smartQuery;
            Parameters = parameters;
            IncludedFields = includedFields;
        }
    }

And the handler

public class
        GetSearchItemsQueryHandler<TIndex> : IRequestHandler<GetSearchItemsQuery<TIndex>,RawEsSearchResponse<TIndex>> where TIndex : SmartSearchableIndex
{
//do stuff here
}

Every time i have the same issue where it says handler couldn't be found for the request. Does anyone know if this is even possible using netcore DI and if it is what magic do i need to do to make it work? Thanks in advance

jbogard commented 3 years ago

What you have is a partially closed generic type. None of the DI containers that I know of can't handle a partially closed type, where the generic parameter you want to fill in is somewhere inside the parameters passed into the overall generic type.

What you'll need to do is explicitly register the closed generic types based on your known TIndex types. Not too horrible, sometimes it's even built into the container registration:

https://lostechies.com/jimmybogard/2010/01/07/advanced-structuremap-custom-registration-conventions-for-partially-closed-types/

craigmoliver commented 3 years ago

I think I am having a similar issue. I have this Handler with generics:

 public class EntityHandlerGet<TDto, TEntity>
    where TDto : class
    where TEntity : class
{
    public class Message : BaseMessage, IRequest<TDto>
    {
        public readonly object[] Id;

        public Message(string correlationId, object[] id) : base(correlationId)
        {
            Id = id;
        }
    }
    public class Handler : IRequestHandler<Message, TDto>
    {
        private readonly MyContext _context;
        private readonly IMapper _mapper;

        public Handler(MyContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        public async Task<TDto> Handle(Message request, CancellationToken cancellationToken)
        {
            var e = await _context.Set<TEntity>().FindAsync(request.Id, cancellationToken);
            if (e == null)
            {
                return null;
            }

            var dto = _mapper.Map<TEntity, TDto>(e);
            return dto;
        }
    }
}

invoked here with generic:

private async Task<OrchestratorResult> DoThing<TEntity, TDto>(
        OrchestratorResult result, 
        string correlationId, 
        TDto dto)
        where TEntity : class, new()
        where TDto : class
    {

        var keyParts = _context.GetKeyParts(entity);
        var getResult = await _mediator.Send(new EntityHandlerGet<TDto, TEntity>.Message(correlationId, keyParts)).ConfigureAwait(false);

        // code that makes OrchestratorResult here, unnecessary for this example

        return result;
    }

from here:

result = await Save<MyEntity, MyEntityDto>(result, correlationId, model.MyEntityDto, transaction);

resulting in this error:

MediatR.IRequestHandler`2[
System.InvalidOperationException : Error constructing handler for request of type MediatR.IRequestHandler`2[ProjectB.Service.Handlers.EntityHandlers.EntityHandlerGet`2+Message[ProjectA.Service.Domain.Models.Dto.MyEntityDto,ProjectA.Service.Domain.Entities.MyEntity],ProjectA.Service.Domain.Models.Dto.MyEntityDto]. Register your handlers with the container. See the samples in GitHub for examples.
---- System.ArgumentException : Implementation type 'ProjectB.Service.Handlers.EntityHandlers.EntityHandlerGet`2[ProjectB.Service.Handlers.EntityHandlers.EntityHandlerGet`2+Message[ProjectA.Service.Domain.Models.Dto.MyEntityDto,ProjectA.Service.Domain.Entities.MyEntity],ProjectA.Service.Domain.Models.Dto.MyEntityDto]' can't be converted to service type 'MediatR.IRequestHandler`2[ProjectB.Service.Handlers.EntityHandlers.EntityHandlerGet`2+Message[ProjectA.Service.Domain.Models.Dto.MyEntityDto,ProjectA.Service.Domain.Entities.MyEntity],ProjectA.Service.Domain.Models.Dto.MyEntityDto]'

I posted on StackOverflow too: https://stackoverflow.com/questions/67321156/mediatr-having-issue-resolving-handler-with-generics-using-microsoft-dependency

jbogard commented 3 years ago

That's a separate issue, and you'll need to post a more complete repro that includes your DI registration.

craigmoliver commented 3 years ago

Here's the mediatr registration

        //mediatr
        services.AddMediatR(typeof(Handlers.BaseMessage).GetTypeInfo().Assembly);
        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
craigmoliver commented 3 years ago

Any insight you can provide would be greatly appreciated. I would be in a great place if I got this one figured.

jbogard commented 3 years ago

What do you see as registered in the container? Are there any conflicts?

craigmoliver commented 3 years ago

Apologies, I don't understand what you mean.

Could it be that the implemented types in the generic types are in different projects?

jbogard commented 3 years ago

No, so when there are "MediatR can't resolve XYZ" problems, I remove MediatR from the equation. Try to resolve that handler manually, from the container. Look at the container's ServiceCollection to make sure that the expected registrations are there.

craigmoliver commented 3 years ago

After some more testing I need to register it with the DI container. I'm not quite sure how to do that with all the generics.

jbogard commented 3 years ago

Open generics tend to need to be registered explicitly. I register some cases, but not all.

jbogard commented 3 years ago

Ah, the readme is wrong, it says I register open IRequestHandler<> but I don't. I'll fix that.

craigmoliver commented 3 years ago

How would you register the sample I posted?

craigmoliver commented 3 years ago

I tried:

services.AddScoped(typeof(IRequest<>), typeof(EntityHandlerGet<,>.Message));
services.AddScoped(typeof(IRequestHandler<,>), typeof(EntityHandlerGet<,>.Handler));
jbogard commented 3 years ago

For your case, you'll likely find that no container supports your case out of the box. The open generic type you have is typeof(EntityHandlerGet<,>.Handler). Nothing that I know of will look at the interface of an inner type like that and try to match it all up. The most they go is typeof(EntityHandlerGet<,>). They won't walk up the type chain to find the open generic and THEN fill it for you.

The best you can do here is loop through your expected entities and DTOs and manually close the types and register the concrete types yourself.

jbogard commented 3 years ago

Alternatively, you can make that EntityHandlerGet type abstract and create concrete types that fill in the generic parameters explicitly. Those will get caught by the type scanners and registered appropriately. Both are work but less hair pulling of containers.

craigmoliver commented 3 years ago

Yeah, I did that originally, I felt the concrete classes defeated the purpose.

craigmoliver commented 3 years ago

Like this?

services.AddScoped(typeof(IRequestHandler<IRequest, Entity>), typeof(EntityHandlerGet<Dto,Entity>.Handler));

jbogard commented 3 years ago

Yes, with the correct request/entity/dto types (not some base types). That's why creating concrete types might be a bit easier than looping through the "possible" types to close the generic types. I've done it before in simple cases (loop through all derived entity types and register say repositories). If it's too complex to loop through the possible types, concrete types even if they have no members or implementation is likely the simplest route.

craigmoliver commented 3 years ago

Got it working. Now that it's working on going to implement the loop through tactic you mentioned before.

        services.AddScoped(typeof(IRequest<MyEntityDto>), typeof(EntityHandlerGet<MyEntityDto, MyEntity>.Message));
        services.AddScoped(typeof(IRequestHandler<EntityHandlerGet<MyEntityDto, MyEntity>.Message, MyEntityDto>), typeof(EntityHandlerGet<MyEntityDto, MyEntity>.Handler));

Thank you for you help and Computer Science lesson.

crowz4k commented 1 year ago

@craigmoliver Did you make the loop work?

maxtheaviator commented 1 year ago

My approach to solve it with AspNet Core DI:

public static class CustomMediatRExtension
{
    public static void AddCustomMediatR(this IServiceCollection services)
    {
        services.AddMediatR(Assembly.GetExecutingAssembly())
            .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

        // Add CommandMessage to classes derived from EntityBase<>
        var entities = GetAllGenericDerivedTypes(typeof(EntityBase<>));
        AddCommandMessage(services, entities);
    }

    private static IEnumerable<Type> GetAllGenericDerivedTypes(Type type)
    {
        var allTypes = Assembly.GetExecutingAssembly().GetTypes();

        return allTypes.Where(t => t.BaseType is { IsGenericType: true }
                                   && t.BaseType.GetGenericTypeDefinition() == type);
    }

    private static void AddCommandMessage(IServiceCollection services, IEnumerable<Type> activatedTypes)
    {
        foreach (var type in activatedTypes)
        {
            var cmdMessageType = typeof(CommandMessage<>).MakeGenericType(type);
            var cmdMessageResponseType = typeof(CommandMessageResponse<>).MakeGenericType(type);
            var cmdMessageHandlerType = typeof(CommandMessageHandler<>).MakeGenericType(type);

            var requestType = typeof(IRequest<>).MakeGenericType(cmdMessageResponseType);
            var requestHandlerType = typeof(IRequestHandler<,>).MakeGenericType(cmdMessageType, cmdMessageResponseType);

            // services.AddTransient<IRequest<TResponse>, TRequest>();
            // Example: services.AddTransient<IRequest<CommandMessageResponse<User>>, CommandMessage<User>>();
            services.AddTransient(requestType, cmdMessageType);

            // services.AddTransient<IRequestHandler<TRequest, TResponse>, TRequestHandler>();
            // Example: services.AddTransient<IRequestHandler<CommandMessage<User>, CommandMessageResponse<User>>,CommandMessageHandler<User>>();
            services.AddTransient(requestHandlerType, cmdMessageHandlerType);
        }
    }
}
public class CommandMessageHandler<T> : IRequestHandler<CommandMessage<T>, CommandMessageResponse<T>>
{
    public Task<CommandMessageResponse<T>> Handle(CommandMessage<T> request, CancellationToken cancellationToken)
    {
        var response = new CommandMessageResponse<T>
        {
            CommandGuid = request.Uuid,
            IsSuccess = true,
            Message = "Hello World!"
        };

        return Task.FromResult(response);
    }
}