Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using multiple FluentValidators on MediatR pipeline

I have a MediatR pipeline behavior like this:

public class FailFastRequestBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly IEnumerable<IValidator> _validators;

    public FailFastRequestBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var failures = _validators
            .Select(async v => await v.ValidateAsync(request))
            .SelectMany(result => result.Result.Errors)
            .Where(f => f != null);


        return failures.Any()
            ? Errors(failures)
            : next();
    }

    ...
}

And MediatR commands like this:

public class MyUseCase
{
    public class Command : IRequest<CommandResponse>
    {
        ...
    }

    public class Validator : AbstractValidator<Command>
    {
        ...
    }

    public class Handler<T>: IRequestHandler<T, CommandResponse>
    {
        ...
    }
}

The validators are registered on Startup.cs like this:

        AssemblyScanner
          .FindValidatorsInAssembly(Assembly.GetAssembly(typeof(MyUseCase)))
            .ForEach(result => 
                services.AddScoped(result.InterfaceType, result.ValidatorType));

This works nice for the MyUseCase.Validator, it is injected on the pipeline and is executed, validating the MyUseCase.Command.

But it's a large application, and many commands have common properties, i.e. every order operation receives an OrderId and I have to check if the Id is valid, if the entity exists in database, if the authenticated user is the owner of the order being modified, etc.

So I tried to create the following interface and validator:

public interface IOrder
{
    string OrderId { get; set; }
}

public class IOrderValidator : AbstractValidator<IOrder>
{
    public IOrderValidator()
    {
        CascadeMode = CascadeMode.StopOnFirstFailure;

        RuleFor(x => x.OrderId)
            .Rule1()
            .Rule2()
            .Rule3()
            .RuleN()
    } 
}

Finally I changed the command to this:

public class MyUseCase
{
    public class Command : IRequest<CommandResponse>: IOrder
    {
        ...
    }

    public class Validator : AbstractValidator<Command>
    {
        ...
    }

    public class Handler<T>: IRequestHandler<T, CommandResponse>
    {
        ...
    }
}

The problem is that the IOrderValidator is not injected in the pipeline, only the MyUseCase.Validator is.

Am I missing something here or is it even possible to inject multiple validators in the pipeline?

like image 996
rbasniak Avatar asked Oct 21 '19 11:10

rbasniak


1 Answers

Service resolution depends on the DI container that you use. It seems that you use built-in .NET Core container and it cannot resolve contravariant interfaces.

Consider Simple Injector instead as it knows how to work with contravariance. This sample code will resolve all the validators you need:

[Fact]
public void Test()
{
    var container = new SimpleInjector.Container();

    container.Collection.Append<IValidator<IOrder>, OrderValidator>();
    container.Collection.Append<IValidator<Command>, CommandValidator>();

    var validators = container.GetAllInstances<IValidator<Command>>();

    validators.Should().HaveCount(2);
}

Alternatively you have to explicitly register your validators parameterized with all commands they must apply to:

[Fact]
public void Test()
{
    var provider = new ServiceCollection()
        .AddTransient(typeof(IValidator<Command>), typeof(OrderValidator))
        .AddTransient(typeof(IValidator<Command>), typeof(CommandValidator))
        .BuildServiceProvider();

    var validators = provider.GetServices<IValidator<Command>>();

    validators.Should().HaveCount(2);
}

Note the difference between IOrder and Command for OrderValidator registration in case of Simple Injector and .NET Core DI container:

container.Collection.Append<IValidator<IOrder>, OrderValidator>();
servcies.AddTransient(typeof(IValidator<Command>), typeof(OrderValidator))

Assuming following classes and interfaces are defined:

interface IOrder
{
}

class Command : IRequest<CommandResponse>, IOrder
{
}

class CommandResponse
{
}

class OrderValidator : AbstractValidator<IOrder>
{
}

class CommandValidator : AbstractValidator<Command>
{
}
like image 119
Andrii Litvinov Avatar answered Nov 02 '22 12:11

Andrii Litvinov