Skip to content
This repository was archived by the owner on Feb 15, 2023. It is now read-only.

Generic Request and Handler With AspNet Core DI #96

Open
vulegend opened this issue Feb 19, 2021 · 21 comments
Open

Generic Request and Handler With AspNet Core DI #96

vulegend opened this issue Feb 19, 2021 · 21 comments

Comments

@vulegend
Copy link

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
Copy link
Owner

jbogard commented Feb 19, 2021

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
Copy link

craigmoliver commented Apr 29, 2021

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
Copy link
Owner

jbogard commented Apr 29, 2021

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

@craigmoliver
Copy link

craigmoliver commented Apr 29, 2021

Here's the mediatr registration

        //mediatr
        services.AddMediatR(typeof(Handlers.BaseMessage).GetTypeInfo().Assembly);
        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

@craigmoliver
Copy link

craigmoliver commented Apr 29, 2021

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

@jbogard jbogard transferred this issue from jbogard/MediatR Apr 29, 2021
@jbogard
Copy link
Owner

jbogard commented Apr 29, 2021

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

@craigmoliver
Copy link

Apologies, I don't understand what you mean.

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

@jbogard
Copy link
Owner

jbogard commented Apr 29, 2021

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
Copy link

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
Copy link
Owner

jbogard commented Apr 29, 2021

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

@jbogard
Copy link
Owner

jbogard commented Apr 29, 2021

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

@craigmoliver
Copy link

How would you register the sample I posted?

@craigmoliver
Copy link

I tried:

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

@jbogard
Copy link
Owner

jbogard commented Apr 29, 2021

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
Copy link
Owner

jbogard commented Apr 29, 2021

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
Copy link

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

@craigmoliver
Copy link

Like this?

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

@jbogard
Copy link
Owner

jbogard commented Apr 29, 2021

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
Copy link

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
Copy link

crowz4k commented Aug 27, 2022

@craigmoliver Did you make the loop work?

@maxtheaviator
Copy link

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);
    }
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants