jbogard / MediatR

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

AddOpenBehavior doesn't register pipeline behaviors with nested generic parameters #1051

Open Mitchman215 opened 1 month ago

Mitchman215 commented 1 month ago

Summary

It seems like AddOpenBehavior doesn't register the pipeline behavior PipelineBehaviorImpl<TRequest, TNested> where TRequest : IRequest<SomeType<TNested>>.

Simple Example

Here is an example to demonstrate the issue when using generic lists:

// Create a new service collection and add mediatr
var services = new ServiceCollection();
services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBehavior(typeof(ListGenericPipelineBehavior<,>));
});

// Build the service provider
var serviceProvider = services.BuildServiceProvider();

using var scope = serviceProvider.CreateScope();
var mediatr = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediatr.Send(new ReturnListRequest());

internal record ReturnListRequest : IRequest<List<string>>;

class ReturnListRequestHandler : IRequestHandler<ReturnListRequest, List<string>>
{
    public Task<List<string>> Handle(ReturnListRequest request, CancellationToken cancellationToken)
    {
        Console.WriteLine("Generic LIST request executed!!");
        List<string> list = ["val"];
        return Task.FromResult(list);
    }
}

internal class ListGenericPipelineBehavior<TRequest, TNested> : IPipelineBehavior<TRequest, List<TNested>>
    where TRequest : IRequest<List<TNested>>
{
    public async Task<List<TNested>> Handle(TRequest request, RequestHandlerDelegate<List<TNested>> next,
        CancellationToken cancellationToken)
    {
        Console.WriteLine($"{this.GetType()}  Before");

        var response = await next();
        Console.WriteLine($"{this.GetType()}  After");
        return response;
    }
}

Running the above code results in the following output:

Generic LIST request executed!!

This means the ListGenericPipelineBehavior was not run since the Before and After messages did not print.

More complicated example

This issue also happens with other types beyond List<T>. My original use case was for pipelines that would apply to some abstract generic base class like BaseCommand<T> below:

// Create a new service collection and add mediatr
var services = new ServiceCollection();
services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBehavior(typeof(CommandPipelineBehavior<,>));
});

// Build the service provider
var serviceProvider = services.BuildServiceProvider();

using var scope = serviceProvider.CreateScope();
var mediatr = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediatr.Send(new StringCommand());

internal record Result<T>(T Value);

internal abstract record BaseCommand<TElement> : IRequest<Result<TElement>>
{
    public Guid Id { get; init; } = Guid.NewGuid();
}

internal record StringCommand : BaseCommand<string>;

internal class StringCommandHandler : IRequestHandler<StringCommand, Result<string>>
{
    public Task<Result<string>> Handle(StringCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine("String Command executed!!");
        return Task.FromResult(new Result<string>("hey"));
    }
}

internal class CommandPipelineBehavior<TCommand, TElement> : IPipelineBehavior<TCommand, Result<TElement>>
    where TCommand : BaseCommand<TElement>
{
    public async Task<Result<TElement>> Handle(TCommand request, RequestHandlerDelegate<Result<TElement>> next,
        CancellationToken cancellationToken)
    {
        Console.WriteLine($"{this.GetType()}  Before, executing command {request.Id}");

        var response = await next();
        // Some more logic based on Result...
        Console.WriteLine($"{this.GetType()}  After, executed command {request.Id}");
        return response;
    }
}

The only output from running the above example is

String Command executed!!

showing that CommandPipelineBehavior didn't run.

Note: while testing this, I noticed that removing the Result<T> type entirely (ie making it so BaseCommand implements IRequest<TElement> instead) resulted in the pipeline behavior being successfully called, so the problem seems to specifically be when the Request's return type has some nested generic parameter itself.

Workaround

Explicitly registering the behaviors with AddBehavior<ListGenericPipelineBehavior<ReturnListRequest, string>>() and AddBehavior<CommandPipelineBehavior<StringCommand, string>>() fixes the issue. This leads me to believe that the root of the problem is with AddOpenBehavior() specifically and not in how the pipelines are looked up and run.

Misc info

The above examples were using MediatR 12.3.0, Microsoft.Extensions.DependencyInjection 8.0.0, and targeting .NET 8 in a Windows environment.

If this is already a known limitation / feature that isn't meant to be implemented, then it'd be nice to document it in the Wiki

sakukk commented 3 weeks ago

I have the same issue