jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
11.19k stars 1.18k forks source link

Question: How to register generic requests #1041

Open rezathecoder opened 5 months ago

rezathecoder commented 5 months ago

Hi I have updated the MediatR package to version 12.3.0 to use the new feature for generic requests and handlers. Is there any other change beside updating the package is needed to enable this feature? I am registering my requests like this:

services.AddMediatR(opts => {
    opts.RegisterServicesFromAssemblyContaining<BaseDto>();
});

But i get the error showing that the container can not find the implementation for my generic handlers. I have two types of generic requests. First the ones that return some data like string or dto like this:

public record GetByIdQuery<TEntity>(int Id) : IRequest<string>
    where TEntity : BaseEntity;

internal sealed class GetByIdQueryHandler<TEntity> : IRequestHandler<GetByIdQuery<TEntity>, string>
    where TEntity : BaseEntity
{

}

And then the ones that return nothing like this:

public record CreateCommand<TEntity>(int Id) : IRequest
    where TEntity : BaseEntity;

internal sealed class CreateCommandHandler<TEntity> : IRequestHandler<CreateCommand<TEntity>>
    where TEntity : BaseEntity
{

}

None of the above types are registered automatically and i have to register them like this:

services.AddTransient<IRequestHandler<GetByIdQuery<MyEntity>, string>, GetByIdQueryHandler<MyEntity>>();

services.AddTransient<IRequestHandler<CreateCommand<MyEntity>>, CreateCommandHandler<MyEntity>>();

Please help me so i can register all of my generic requests at once. @jbogard @zachpainter77

zachpainter77 commented 5 months ago

Well. My initial thought without being able to test out your class definitions are that you are declaring the handlers as internal and then trying to add mediatr from an assembly that doesn't have visibility to internals. The fix to this would be to change the internal visibility or to make internals visible to the other calling assembly. I don't know if this is the exact cause or not. I will confirm this soon. But, this is my initial thought.

zachpainter77 commented 5 months ago

Yes I do believe the issue with your handlers is that you are declaring the handler classes as internal. I am guessing you are calling AddMediatR from your .net core presentation layer project, such as, mvc, or blazor, or razor pages, etc... Probably from the Program.cs class. The issue is that your presentation layer, being in a separate assembly cannot see the internal handler when doing the assembly scanning and therefore never registers those handlers.

You will need to update the handler's access modifier to allow other assemblies to see it. Here is more info.

Another option is to make those internals visible to another assembly. You can do this by adding this to the library you want to make visible.

[assembly: InternalsVisibleTo("NameOfAssemblyYouWantToMakeLibraryVisibleTo")]
rezathecoder commented 5 months ago

Yes I do believe the issue with your handlers is that you are declaring the handler classes as internal. I am guessing you are calling AddMediatR from your .net core presentation layer project, such as, mvc, or blazor, or razor pages, etc... Probably from the Program.cs class. The issue is that your presentation layer, being in a separate assembly cannot see the internal handler when doing the assembly scanning and therefore never registers those handlers.

You will need to update the handler's access modifier to allow other assemblies to see it. Here is more info.

Another option is to make those internals visible to another assembly. You can do this by adding this to the library you want to make visible.

[assembly: InternalsVisibleTo("NameOfAssemblyYouWantToMakeLibraryVisibleTo")]

No sir this is not the issue. The assembly that contains the internal handlers is responsible for registering mediatr and has access to them. The same approach is applied for non-generic requests and handlers. That means public requests and internal handlers but those are registered successfully and it's working

zachpainter77 commented 5 months ago

So I can see that your sample registers handlers in the assembly that contains the BaseDto class. Are you saying that the base dto assembly has access to the internals of the assembly that contains the handlers? Are your handlers in the same assembly as the BaseDto type? Maybe it would help me understand more if you shared your project architecture and structure and note which classes are in what assembly.

On Sat, Jun 22, 2024, 1:35 PM rezathecoder @.***> wrote:

Yes I do believe the issue with your handlers is that you are declaring the handler classes as internal. I am guessing you are calling AddMediatR from your .net core presentation layer project, such as, mvc, or blazor, or razor pages, etc... Probably from the Program.cs class. The issue is that your presentation layer, being in a separate assembly cannot see the internal handler when doing the assembly scanning and therefore never registers those handlers.

You will need to update the handler's access modifier to allow other assemblies to see it. Here https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers is more info.

Another option is to make those internals visible to another assembly. You can do this by adding this to the library you want to make visible.

[assembly: InternalsVisibleTo("NameOfAssemblyYouWantToMakeLibraryVisibleTo")]

No sir this is not the issue. The assembly that contains the internal handlers is responsible for registering mediatr and has access to them. The same principles is done for non-generic requests and handlers. That means public requests and internal handlers but those are registered successfully and it's working

— Reply to this email directly, view it on GitHub https://github.com/jbogard/MediatR/issues/1041#issuecomment-2184181245, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACL2QUS5LQVLXMMINVLXMZTZIXNZFAVCNFSM6AAAAABJNYPNIKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCOBUGE4DCMRUGU . You are receiving this because you were mentioned.Message ID: @.***>

rezathecoder commented 5 months ago

So I can see that your sample registers handlers in the assembly that contains the BaseDto class. Are you saying that the base dto assembly has access to the internals of the assembly that contains the handlers? Are your handlers in the same assembly as the BaseDto type? Maybe it would help me understand more if you shared your project architecture and structure and note which classes are in what assembly. On Sat, Jun 22, 2024, 1:35 PM rezathecoder @.> wrote: Yes I do believe the issue with your handlers is that you are declaring the handler classes as internal. I am guessing you are calling AddMediatR from your .net core presentation layer project, such as, mvc, or blazor, or razor pages, etc... Probably from the Program.cs class. The issue is that your presentation layer, being in a separate assembly cannot see the internal handler when doing the assembly scanning and therefore never registers those handlers. You will need to update the handler's access modifier to allow other assemblies to see it. Here https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers is more info. Another option is to make those internals visible to another assembly. You can do this by adding this to the library you want to make visible. [assembly: InternalsVisibleTo("NameOfAssemblyYouWantToMakeLibraryVisibleTo")] No sir this is not the issue. The assembly that contains the internal handlers is responsible for registering mediatr and has access to them. The same principles is done for non-generic requests and handlers. That means public requests and internal handlers but those are registered successfully and it's working — Reply to this email directly, view it on GitHub <#1041 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACL2QUS5LQVLXMMINVLXMZTZIXNZFAVCNFSM6AAAAABJNYPNIKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCOBUGE4DCMRUGU . You are receiving this because you were mentioned.Message ID: @.>

Yes the BaseDto and all requests and request handlers are in the same assembly called Application layer. The follwing code which is responsible of registering MediatR is also located in Application layer:

public static void RegisterApplicationLayer(this IServiceCollection services) {
    services.AddMediatR(opts => {  
    opts.RegisterServicesFromAssemblyContaining<BaseDto>();
});
}

Then in program.cs file in Api layer i simply call:

builder.Services.RegisterApplicationLayer();

Non-generic requests and handlers are registered fine but the generics not

zachpainter77 commented 5 months ago

Hmm.. Ok.. So if what you say is accurate then I have no idea why the handlers are not being registered.

In my testcase for this specific issue I created a class in a separate project to be used as your "ApplicationLayer" example.

 public class BaseEntity
 {
     public int Id { get; set; }       
 }

 public class  Entity : BaseEntity
 {

 }

 public record GetByIdQuery<TEntity>(int Id) : IRequest<string>
 where TEntity : BaseEntity;

 internal sealed class GetByIdQueryHandler<TEntity> : IRequestHandler<GetByIdQuery<TEntity>, string>
     where TEntity : BaseEntity
 {
     public Task<string> Handle(GetByIdQuery<TEntity> request, CancellationToken cancellationToken)
     {
         return Task.FromResult(request.Id.ToString());
     }
 }

 public static class Registration
 {
     public static IServiceCollection RegisterApplicationLayer(this IServiceCollection services)
     {
         return services.AddMediatR(opts => opts.RegisterServicesFromAssemblyContaining<BaseEntity>());

     }
 }

This contains the BaseEntity definition, a class Entity that extends that base class, the generic request definition with constraints, the generic handler definition for that request, and a static registration extension method that calls registers mediatR like you suggest above.

Lastly I simply call the extension method from my Program.cs file in my presentation layer:

builder.Services.RegisterApplicationLayer();

In my presentation layer I then call the mediatR request in an action method on a controller like so:

 public async Task<IActionResult> Index()
 {            
     var vm = new HomeViewModel
     {                
         IdValue = await _mediator.Send(new GetByIdQuery<Entity>(1))
     };
     return View(vm);
 }

The result is successful and the handler is registered. The view shows the input id that is stored on the view model.

So I'm really not sure what else could be different between my test and your issue? It seems to work as designed on my end.

zachpainter77 commented 4 months ago

@rezathecoder is this still an issue? Did you ever get to the bottom of this?

janhruban commented 4 months ago

I had the same problem. It looks like if the generic parameter is in a different assembly, you need to add this assembly during the registration:

configuration.RegisterServicesFromAssemblies(typeof(Request<>).Assembly, typeof(Entity).Assembly)

rezathecoder commented 3 months ago

I had the same problem. It looks like if the generic parameter is in a different assembly, you need to add this assembly during the registration:

configuration.RegisterServicesFromAssemblies(typeof(Request<>).Assembly, typeof(Entity).Assembly)

Today i had time and debugged into MediatR code and reached the exact same solution and wanted to share it with others but you mentioned it earlier 😆😆😆 Anyway. To complete your solution the code that causes this problem is this line:

https://github.com/jbogard/MediatR/blob/cac76bee59a0f61c99554d76d4c39d67810fb406/src/MediatR/Registration/ServiceRegistrar.cs#L236 https://github.com/jbogard/MediatR/blob/cac76bee59a0f61c99554d76d4c39d67810fb406/src/MediatR/Registration/ServiceRegistrar.cs#L237

This tries to get all possible types for registering the handler based on the constraints. In my case my matching types are my entities that are present in the Domain layer and assembliesToScan will not search for them. This should be mentioned in the document @zachpainter77

zachpainter77 commented 3 months ago

Yes you must pass in your domain layer assembly

On Sat, Aug 24, 2024, 7:56 AM rezathecoder @.***> wrote:

I had the same problem. It looks like if the generic parameter is in a different assembly, you need to add this assembly during the registration:

configuration.RegisterServicesFromAssemblies(typeof(Request<>).Assembly, typeof(Entity).Assembly)

Today i had time and debugged into MediatR code and reached the exact same solution and wanted to share it with others but you mentioned it earlier 😆😆😆 Anyway. To complete your solution the code that causes this problem is this line:

https://github.com/jbogard/MediatR/blob/cac76bee59a0f61c99554d76d4c39d67810fb406/src/MediatR/Registration/ServiceRegistrar.cs#L236

https://github.com/jbogard/MediatR/blob/cac76bee59a0f61c99554d76d4c39d67810fb406/src/MediatR/Registration/ServiceRegistrar.cs#L237

This tries to get all possible types for registering the handler based on the constraints. In my case my matching types are my entities that are present in the Domain layer and assembliesToScan will not search for them. This should be mentioned in the document @zachpainter77 https://github.com/zachpainter77

— Reply to this email directly, view it on GitHub https://github.com/jbogard/MediatR/issues/1041#issuecomment-2308421687, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACL2QUVWG6JWSNI3CZURPD3ZTCNKDAVCNFSM6AAAAABJNYPNIKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGMBYGQZDCNRYG4 . You are receiving this because you were mentioned.Message ID: @.***>

dinarplay commented 1 month ago

Can anyone help me, this is not working, even the above code. I even got all the assemblies of the solution and pass in them all, it still didn't help

lstsystems commented 4 weeks ago

i have a similar issue and the above code did not solve it either even when passing both assemblies

Service Registration

services.AddMediatR(cfg =>
        {
            cfg.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyReference).Assembly, typeof(DomainAssemblyReference).Assembly);
            cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
            cfg.AddOpenBehavior(typeof(FluentValidationBehavior<,>));
        });

Command:

public record CreatePageCommand<TPage, TRequestDto>(TRequestDto Page, string ContextUsername)
    : IRequest<Result<int>> where TPage : Page where TRequestDto : IPageRequest;

public class CreatePageCommandHandler<TPage, TRequestDto>(
    IApplicationDbContext dbContext, 
    IPageValidationService<TPage, TRequestDto> pageValidationService) 
    : IRequestHandler<CreatePageCommand<TPage, TRequestDto>, Result<int>> 
    where TPage : Page 
    where TRequestDto : IPageRequest
{
    public async Task<Result<int>> Handle(CreatePageCommand<TPage, TRequestDto> request, CancellationToken cancellationToken)
    {
    }
}

the only time it works is when i register the different versions of the command individually.

services.AddTransient<IRequestHandler<CreatePageCommand<DocumentPage, DocumentPageRequestDto>, Result<int>>, CreatePageCommandHandler<DocumentPage, DocumentPageRequestDto>>();
vs-savelich commented 2 weeks ago

@lstsystems the latest release changed autoregistration of generic request handlers feature to OPT-IN. You have to enable it when registering MediatR:

services.AddMediatR(cfg =>
    {
        cfg.RegisterGenericHandlers = true;
        ...
    }
);

Works like a charm :)

lstsystems commented 2 weeks ago

@vs-savelich thanks that worked like a charm👍.

One thing to note is that I initially registered each iteration of IPageValidationService<TPage, TRequestDto> manually, like this: services.AddScoped<IPageValidationService<DocumentPage, DocumentPageRequestDto>, PageValidationService<DocumentPage, DocumentPageRequestDto>>();. This caused issues until I also registered the service generically: services.AddScoped(typeof(IPageValidationService<,>), typeof(PageValidationService<,>)); when cfg.RegisterGenericHandlers = true; was enabled. I discovered this by chance, as I normally wouldn't register all variations manually. It appears that if you have a generic service inside a generic command, it expects a generic service registration; otherwise, it fails to resolve properly.