Skip to content

Commit

Permalink
Fix issue with how CascadeMode.Stop works when set inside a child val…
Browse files Browse the repository at this point in the history
…idator
  • Loading branch information
JeremySkinner committed Apr 23, 2024
1 parent a5a5700 commit a0eaeae
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 6 deletions.
3 changes: 3 additions & 0 deletions Changelog.txt
@@ -1,3 +1,6 @@
11.9.1 - 23 Apr 2024
Fix issue with CascadeMode on child validators (#2207)

11.9.0 - 21 Dec 2023
Fix memory leak in NotEmptyValidator/EmptyValidator (#2174)
Add more descriptive error messages if a rule throws a NullReferenceException (#2152)
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>11.9.0</VersionPrefix>
<VersionPrefix>11.9.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<!-- Use CI build number as version suffix (if defined) -->
<!--<VersionSuffix Condition="'$(GITHUB_RUN_NUMBER)'!=''">ci-$(GITHUB_RUN_NUMBER)</VersionSuffix>-->
Expand Down
29 changes: 29 additions & 0 deletions src/FluentValidation.Tests/CascadingFailuresTester.cs
Expand Up @@ -21,6 +21,7 @@
namespace FluentValidation.Tests;

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -490,6 +491,34 @@ public class CascadingFailuresTester : IDisposable {
results.Errors.Count.ShouldEqual(1);
}

[Fact]
public async void Cascade_set_to_stop_in_child_validator_with_RuleForEach_in_parent() {
// See https://github.com/FluentValidation/FluentValidation/issues/2207

var childValidator = new InlineValidator<Order>();
childValidator.ClassLevelCascadeMode = CascadeMode.Stop;
childValidator.RuleFor(x => x.ProductName).NotNull();
childValidator.RuleFor(x => x.Amount).GreaterThan(0);

var parentValidator = new InlineValidator<Person>();
parentValidator.RuleForEach(x => x.Orders).SetValidator(childValidator);

var testData = new List<Order> {
// Would cause both rules to fail, but only first rule will be executed because of CascadeMode.Stop
new Order { ProductName = null, Amount = 0 },

// First rule succeeds, second rule fails.
new Order { ProductName = "foo", Amount = 0 }
};

// Bug in #2207 meant that the rule for Orders[1].Amount would never execute
// as the cascade mode logic was stopping if totalFailures > 0 rather than totalFailures > (count of failures before rule executed)
var result = parentValidator.Validate(new Person {Orders = testData});
result.Errors.Count.ShouldEqual(2);
result.Errors[0].PropertyName.ShouldEqual("Orders[0].ProductName");
result.Errors[1].PropertyName.ShouldEqual("Orders[1].Amount");
}

private void SetBothValidatorCascadeModes(CascadeMode cascadeMode) {
_validator.ClassLevelCascadeMode = cascadeMode;
_validator.RuleLevelCascadeMode = cascadeMode;
Expand Down
10 changes: 5 additions & 5 deletions src/FluentValidation/AbstractValidator.cs
Expand Up @@ -241,13 +241,13 @@ ValueTask<ValidationResult> completedValueTask
// Performance: Use for loop rather than foreach to reduce allocations.
for (int i = 0; i < count; i++) {
cancellation.ThrowIfCancellationRequested();
var totalFailures = context.Failures.Count;

await Rules[i].ValidateAsync(context, useAsync, cancellation);

if (ClassLevelCascadeMode == CascadeMode.Stop && result.Errors.Count > 0) {
// Bail out if we're "failing-fast".
// Check for > 0 rather than == 1 because a rule chain may have overridden the Stop behaviour to Continue
// meaning that although the first rule failed, it actually generated 2 failures if there were 2 validators
// in the chain.
if (ClassLevelCascadeMode == CascadeMode.Stop && result.Errors.Count > totalFailures) {
// Bail out if we're "failing-fast". Check to see if the number of failures
// has been increased by this rule (which could've generated 1 or more failures).
break;
}
}
Expand Down

0 comments on commit a0eaeae

Please sign in to comment.