FluentValidation rules chaining not stopping at first failure
Asked Answered
S

4

14

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

Sandwich answered 7/2, 2017 at 21:23 Comment(2)
I imagine it's complaining because int[] is a non-nullable type?Faithfaithful
@EricSondergard the array can be null as I think it's a reference type. But yes, the actual int values are non-nullableSandwich
S
14

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.

Sandwich answered 8/2, 2017 at 8:47 Comment(0)
N
17

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;
}
Necrosis answered 10/9, 2020 at 14:5 Comment(2)
this answer saved my dayCarpology
If StatementItems is int[]? instead of int[] the compiler still gives me a warning, that 'x' may be null here (CS8602 dereference of a possibly null reference). How can we handle this warning?Oran
S
14

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.

Sandwich answered 8/2, 2017 at 8:47 Comment(0)
D
2

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

Descriptive answered 7/2, 2017 at 22:18 Comment(0)
D
0
CascadeMode = CascadeMode.Stop //This is obselete now

Use this :

    RuleLevelCascadeMode = CascadeMode.Stop;
Diantha answered 17/5, 2024 at 10:6 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.