ADringer
ADringer

Reputation: 2834

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

Upvotes: 17

Views: 11394

Answers (5)

System.Cats.Lol
System.Cats.Lol

Reputation: 1770

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

Upvotes: 19

Rissa
Rissa

Reputation: 201

1- on constructor put this code

this.ClassLevelCascadeMode = CascadeMode.Stop;

means next rulefor not implemented only if first rulefor is correct.

2- rulefor PincodeIsUnique not implemented only if first condition is correc bec(.Cascade(CascadeMode.Stop)) and in first rulefor conditions are implemented as one by one.

RuleFor(c => c.pincode)
        .Cascade(CascadeMode.Stop)
        .NotEmpty().WithMessage("Pincode is required.")
        .NotNull()
        .Length(3, 6).WithMessage("Pincode between 3 and 6 characters.")
        .Must(BeAValidPincode).WithMessage("Please specify a valid pincode.");

    RuleFor(c => c)
       .MustAsync(PincodeIsUnique)
       .WithMessage("A picode with the same number already exists.");

Upvotes: 1

Wasim
Wasim

Reputation: 724

CascadeMode = CascadeMode.Stop //This is obselete now

Use this :

    RuleLevelCascadeMode = CascadeMode.Stop;

Upvotes: 0

ADringer
ADringer

Reputation: 2834

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.

Upvotes: 14

NPras
NPras

Reputation: 4125

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..

Upvotes: 2

Related Questions