Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FluentValidation rules chaining not stopping at first failure

I have a model:

public class DTO
{
    public int[] StatementItems { get; set; }
}

Which I want to validate that:

  1. StatementItems is not null
  2. StatementItems is not empty
  3. StatementItems does not contain any duplicate IDs

The validation rule chain I created is:

RuleFor(x => x.StatementItems).NotNull().NotEmpty().Must(x => x.Distinct().Count() == x.Count());

And I have a test as:

_validator.ShouldHaveValidationErrorFor(x => x.StatementItems, null as int[]);

When I run the test passing in a null value, I would expect it to fail on the first rule of the chain (NotNull()) and stop there. However, it complains that the lamda value used in the Must() is null.

Am I wrong in thinking that the Must() shouldn't be run if the NotNull() fails? If so, how should this rule be written?

Thanks

like image 780
ADringer Avatar asked Feb 07 '17 21:02

ADringer


3 Answers

Check out FluentValidation's cascade mode. You can make it short-circuit on the first failure like this:

this.RuleFor(x => x.StatementItems)
   .Cascade(CascadeMode.Stop)
   .NotNull()
   .NotEmpty()
   .Must(x => x.Distinct().Count() == x.Count());

Also, you can configure this in your AbstractValidator subclass's constructor. Then you won't need to put it on every rule.

public MyInputValidator()
{
  this.CascadeMode = CascadeMode.Stop;
}
like image 158
System.Cats.Lol Avatar answered Nov 18 '22 18:11

System.Cats.Lol


Although @NPras's answer did supply my with a solution, I didn't like the fact that I'm duplicating the NotNull rule. After a bit more research on FluentValidation I have implemented it using DependentRules:

RuleFor(x => x.StatementItems).NotNull().NotEmpty()
            .DependentRules(d =>
                d.RuleFor(x => x.StatementItems).Must(x => x.Distinct().Count() == x.Count())
            );

So now the Must condition is only fired when the previous two rules are valid.

like image 45
ADringer Avatar answered Nov 18 '22 19:11

ADringer


I don't see in the FluentValidation documentation that it actually guarantees short-circuiting.

If you look in its source:

public virtual ValidationResult Validate(ValidationContext<T> context)
{
  ...
  var failures = nestedValidators.SelectMany(x => x.Validate(context));
  return new ValidationResult(failures);
}

It will run through *all* the validators (with the SelectMany()) and returns a list of errors.

Your only option seems to be to force a check on your Must rule.

.Must(x => x!= null && x.Distinct().Count() == x.Count())
//or, fluently:
.Must(x => x.Distinct().Count() == x.Count()).When(x => x! = null)

EDIT: I was going to suggest that since Validate() is virtual, you could just override it in your validator to make it short-circuit. But then I realised that the nestedValidators list is private. So yeah, no..

like image 25
NPras Avatar answered Nov 18 '22 17:11

NPras