Search code examples
c#dependency-injection.net-6.0mediatr

MS DI + MediatR: PipelineBehavior for a base IRequest with a generic response


I'm trying to add a pipeline behavior on a base request that validates that the invoker has access to the tenant they are invoking against.

One way I found, is adding an interface ITenantRequest on some SomethingRequest and making a completely generic PipelineBehavior<,> that will be invoked on every single request and perform the validation if request is ITenantRequest.

That doesn't seems very efficient though and I'm wondering if I could make the pipeline run only on requests that are extending a base class with that property.

Here's the code that I wish worked, but it breaks with the following error message:

Implementation type 'TenantAccessValidationPipeline`2[SomeTenantRequest,SomeTenantResponse]' can't be converted to service type 'MediatR.IPipelineBehavior`2[SomeTenantRequest,SomeTenantResponse]'

Bootstrapping:

var services = new ServiceCollection();
services.AddMediatR(typeof(Program).Assembly);
services.AddScoped(typeof(IPipelineBehavior<,>),
    typeof(TenantAccessValidationPipeline<,>));

var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();

mediator.Send(new SomeTenantRequest() {
    SomeProperty = "SomeProperty", TenantId = "CustomerId" });

Classes:

public class RequestsBase<T> : IRequest<T>
{
    public string TenantId { get; set; }
}

public class TenantAccessValidationPipeline<T, _>
    : IPipelineBehavior<RequestsBase<T>, T>
{
    public Task<T> Handle(RequestsBase<T> request, CancellationToken ct,
        RequestHandlerDelegate<T> next)
    {
        Console.WriteLine(request.TenantId);
        return next();
    }
}

public record SomeTenantResponse(string p);

public class SomeTenantRequest : RequestsBase<SomeTenantResponse>
{
    public string SomeProperty { get; set; }
}

public class SomeTenantRequestHandler
    : IRequestHandler<SomeTenantRequest, SomeTenantResponse>
{
    public Task<SomeTenantResponse> Handle(
        SomeTenantRequest request, CancellationToken ct)
    {
        return Task.FromResult(
            new SomeTenantResponse(request.TenantId + request.SomeProperty));
    }
}

Solution

  • Your TenantAccessValidationPipeline type has two generic parameters, but the second one, _, is unmapped to its interface:

    public class TenantAccessValidationPipeline<T, _>
        : IPipelineBehavior<RequestsBase<T>, T>
    

    Because of this, no DI container in the world will be able to resolve a closed-version of TenantAccessValidationPipeline<T, _> for you. It's simply impossible to guess what specific typee must be filled in for _.

    If you weren't using MS.DI, this problem can be solved by simply removing the second generic type argument:

    public class TenantAccessValidationPipeline<TResponse>
        : IPipelineBehavior<RequestsBase<TResponse>, TResponse>
    

    Although this will work with some DI Containers, not so much with MS.DI, because MS.DI's generic type system is really naive, as I explained recently here.

    MS.DI requires the generic type arguments of the implementation to line up with that of its abstraction. So the workaround here is to apply a 'where' generic type constraint:

    public class TenantAccessValidationPipeline<TRequest, TResponse>
        : IPipelineBehavior<TRequest, TResponse>
        where TRequest : RequestsBase<TResponse>
    

    This works -solely- because MediatR will resolve the pipeline behaviors as a collection. MS.DI supports generic type constraints on collections - but only on collections.