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
Summary
It seems like
AddOpenBehavior
doesn't register the pipeline behaviorPipelineBehaviorImpl<TRequest, TNested>
whereTRequest : IRequest<SomeType<TNested>>
.Simple Example
Here is an example to demonstrate the issue when using generic lists:
Running the above code results in the following output:
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 likeBaseCommand<T>
below:The only output from running the above example is
showing that
CommandPipelineBehavior
didn't run.Note: while testing this, I noticed that removing the
Result<T>
type entirely (ie making it soBaseCommand
implementsIRequest<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>>()
andAddBehavior<CommandPipelineBehavior<StringCommand, string>>()
fixes the issue. This leads me to believe that the root of the problem is withAddOpenBehavior()
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