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));
}
}
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.