Open vulegend opened 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:
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
That's a separate issue, and you'll need to post a more complete repro that includes your DI registration.
Here's the mediatr registration
//mediatr
services.AddMediatR(typeof(Handlers.BaseMessage).GetTypeInfo().Assembly);
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
Any insight you can provide would be greatly appreciated. I would be in a great place if I got this one figured.
What do you see as registered in the container? Are there any conflicts?
Apologies, I don't understand what you mean.
Could it be that the implemented types in the generic types are in different projects?
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.
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.
Open generics tend to need to be registered explicitly. I register some cases, but not all.
Ah, the readme is wrong, it says I register open IRequestHandler<>
but I don't. I'll fix that.
How would you register the sample I posted?
I tried:
services.AddScoped(typeof(IRequest<>), typeof(EntityHandlerGet<,>.Message));
services.AddScoped(typeof(IRequestHandler<,>), typeof(EntityHandlerGet<,>.Handler));
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.
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.
Yeah, I did that originally, I felt the concrete classes defeated the purpose.
Like this?
services.AddScoped(typeof(IRequestHandler<IRequest
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.
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.
@craigmoliver Did you make the loop work?
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);
}
}
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 :
And the handler
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