diff --git a/Changelog.txt b/Changelog.txt index 6ce0c68e2..7502e3f73 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -5,6 +5,9 @@ Removes deprecated DI extensions Removes deprecated transform methods (#2027) Remove the ability to disable the root-model null check (#2069) +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) diff --git a/docs/requirements_rtd.txt b/docs/requirements_rtd.txt index f4bf850ec..106c4bdf0 100644 --- a/docs/requirements_rtd.txt +++ b/docs/requirements_rtd.txt @@ -1,5 +1,5 @@ mock==1.0.1 -alabaster>=0.7,<0.8,!=0.7.5 +alabaster==0.7.13 commonmark==0.9.1 recommonmark==0.5.0 readthedocs-sphinx-ext<2.3 @@ -7,4 +7,4 @@ jinja2<3.1.0 recommonmark==0.5.0 sphinx==1.8.5 sphinx-rtd-theme==0.4.3 -docutils==0.17 \ No newline at end of file +docutils==0.17 diff --git a/src/FluentValidation.Tests/CascadingFailuresTester.cs b/src/FluentValidation.Tests/CascadingFailuresTester.cs index 9c704681f..de3ebf90a 100644 --- a/src/FluentValidation.Tests/CascadingFailuresTester.cs +++ b/src/FluentValidation.Tests/CascadingFailuresTester.cs @@ -21,6 +21,7 @@ namespace FluentValidation.Tests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -447,6 +448,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(); + childValidator.ClassLevelCascadeMode = CascadeMode.Stop; + childValidator.RuleFor(x => x.ProductName).NotNull(); + childValidator.RuleFor(x => x.Amount).GreaterThan(0); + + var parentValidator = new InlineValidator(); + parentValidator.RuleForEach(x => x.Orders).SetValidator(childValidator); + + var testData = new List { + // 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"); + } + [Fact] public void CascadeMode_values_should_correspond_to_correct_integers() { // 12.0 removed the "StopOnFirstFailure" option which was value 1. diff --git a/src/FluentValidation.Tests/ValidatorTesterTester.cs b/src/FluentValidation.Tests/ValidatorTesterTester.cs index 543cf1ba9..ea6c15a2e 100644 --- a/src/FluentValidation.Tests/ValidatorTesterTester.cs +++ b/src/FluentValidation.Tests/ValidatorTesterTester.cs @@ -30,6 +30,7 @@ public class ValidatorTesterTester { private TestValidator validator; public ValidatorTesterTester() { + CultureScope.SetDefaultCulture(); validator = new TestValidator(); validator.RuleFor(x => x.Forename).NotNull(); validator.RuleForEach(person => person.NickNames).MinimumLength(5); @@ -954,15 +955,15 @@ public class ValidatorTesterTester { [Fact] public void ShouldHaveValidationErrorFor_WithPropertyName_Only_throws() { var validator = new InlineValidator(); - validator.RuleFor(x => DateTime.Now) + validator.RuleFor(x => x.Age) .Must((x, ct) => false) - .LessThan(new DateTime(1900, 1, 1)); + .LessThan(50); Assert.Throws(() => - validator.TestValidate(new Person()) - .ShouldHaveValidationErrorFor("Now") - .WithErrorMessage("The specified condition was not met for 'Now'.") + validator.TestValidate(new Person { Age = 100 }) + .ShouldHaveValidationErrorFor("Age") + .WithErrorMessage("The specified condition was not met for 'Age'.") .Only() - ).Message.ShouldEqual("Expected to have errors only matching specified conditions\n----\nUnexpected Errors:\n[0]: 'Now' must be less than '1/1/1900 12:00:00 AM'.\n"); + ).Message.ShouldEqual("Expected to have errors only matching specified conditions\n----\nUnexpected Errors:\n[0]: 'Age' must be less than '50'.\n"); } [Fact] diff --git a/src/FluentValidation/AbstractValidator.cs b/src/FluentValidation/AbstractValidator.cs index 479ad9d9c..381cd249f 100644 --- a/src/FluentValidation/AbstractValidator.cs +++ b/src/FluentValidation/AbstractValidator.cs @@ -173,13 +173,13 @@ ValueTask 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; } } diff --git a/src/FluentValidation/Resources/LanguageManager.cs b/src/FluentValidation/Resources/LanguageManager.cs index 7914940f5..aebc780be 100644 --- a/src/FluentValidation/Resources/LanguageManager.cs +++ b/src/FluentValidation/Resources/LanguageManager.cs @@ -71,6 +71,7 @@ public class LanguageManager : ILanguageManager { KoreanLanguage.Culture => KoreanLanguage.GetTranslation(key), MacedonianLanguage.Culture => MacedonianLanguage.GetTranslation(key), NorwegianBokmalLanguage.Culture => NorwegianBokmalLanguage.GetTranslation(key), + NorwegianNynorskLanguage.Culture => NorwegianNynorskLanguage.GetTranslation(key), PersianLanguage.Culture => PersianLanguage.GetTranslation(key), PolishLanguage.Culture => PolishLanguage.GetTranslation(key), PortugueseLanguage.Culture => PortugueseLanguage.GetTranslation(key), diff --git a/src/FluentValidation/Resources/Languages/GermanLanguage.cs b/src/FluentValidation/Resources/Languages/GermanLanguage.cs index ad0be2550..28ff284e6 100644 --- a/src/FluentValidation/Resources/Languages/GermanLanguage.cs +++ b/src/FluentValidation/Resources/Languages/GermanLanguage.cs @@ -43,7 +43,7 @@ internal class GermanLanguage { "EqualValidator" => "'{PropertyName}' muss gleich '{ComparisonValue}' sein.", "ExactLengthValidator" => "'{PropertyName}' muss genau {MaxLength} lang sein. Es wurden {TotalLength} eingegeben.", "ExclusiveBetweenValidator" => "'{PropertyName}' muss zwischen {From} und {To} sein (exklusiv). Es wurde {PropertyValue} eingegeben.", - "InclusiveBetweenValidator" => "'{PropertyName}' muss zwischen {From} and {To} sein. Es wurde {PropertyValue} eingegeben.", + "InclusiveBetweenValidator" => "'{PropertyName}' muss zwischen {From} und {To} sein. Es wurde {PropertyValue} eingegeben.", "CreditCardValidator" => "'{PropertyName}' ist keine gültige Kreditkartennummer.", "ScalePrecisionValidator" => "'{PropertyName}' darf insgesamt nicht mehr als {ExpectedPrecision} Ziffern enthalten, mit Berücksichtigung von {ExpectedScale} Dezimalstellen. Es wurden {Digits} Ziffern und {ActualScale} Dezimalstellen gefunden.", "EmptyValidator" => "'{PropertyName}' sollte leer sein.", @@ -54,7 +54,7 @@ internal class GermanLanguage { "MinimumLength_Simple" => "Die Länge von '{PropertyName}' muss größer oder gleich {MinLength} sein.", "MaximumLength_Simple" => "Die Länge von '{PropertyName}' muss kleiner oder gleich {MaxLength} sein.", "ExactLength_Simple" => "'{PropertyName}' muss genau {MaxLength} lang sein.", - "InclusiveBetween_Simple" => "'{PropertyName}' muss zwischen {From} and {To} sein.", + "InclusiveBetween_Simple" => "'{PropertyName}' muss zwischen {From} und {To} sein.", _ => null, }; } diff --git a/src/FluentValidation/Resources/Languages/NorwegianNynorskLanguage.cs b/src/FluentValidation/Resources/Languages/NorwegianNynorskLanguage.cs new file mode 100644 index 000000000..5c5bf5ff9 --- /dev/null +++ b/src/FluentValidation/Resources/Languages/NorwegianNynorskLanguage.cs @@ -0,0 +1,60 @@ +#region License + +// Copyright (c) .NET Foundation and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation + +#endregion + +#pragma warning disable 618 + +namespace FluentValidation.Resources; + +internal class NorwegianNynorskLanguage { + public const string Culture = "nn"; + + public static string GetTranslation(string key) => key switch { + "EmailValidator" => "'{PropertyName}' er ikkje ei gyldig e-postadresse.", + "GreaterThanOrEqualValidator" => "'{PropertyName}' skal vera større enn eller lik '{ComparisonValue}'.", + "GreaterThanValidator" => "'{PropertyName}' skal vera større enn '{ComparisonValue}'.", + "LengthValidator" => "'{PropertyName}' skal vere mellom {MinLength} og {MaxLength} teikn. Du har tasta inn {TotalLength} teikn.", + "MinimumLengthValidator" => "'{PropertyName}' skal vera større enn eller lik {MinLength} teikn. Du tasta inn {TotalLength} teikn.", + "MaximumLengthValidator" => "'{PropertyName}' skal vera mindre enn eller lik {MaxLength} teikn. Du tasta inn {TotalLength} teikn.", + "LessThanOrEqualValidator" => "'{PropertyName}' skal vera mindre enn eller lik '{ComparisonValue}'.", + "LessThanValidator" => "'{PropertyName}' skal vera mindre enn '{ComparisonValue}'.", + "NotEmptyValidator" => "'{PropertyName}' kan ikkje vera tom.", + "NotEqualValidator" => "'{PropertyName}' kan ikkje vera lik '{ComparisonValue}'.", + "NotNullValidator" => "'{PropertyName}' kan ikkje vera tom.", + "PredicateValidator" => "Den angjevne føresetnaden var ikkje oppfylt for '{PropertyName}'.", + "AsyncPredicateValidator" => "Den angjevne føresetnaden var ikkje oppfylt for '{PropertyName}'.", + "RegularExpressionValidator" => "'{PropertyName}' har ikkje rett format.", + "EqualValidator" => "'{PropertyName}' skal vera lik '{ComparisonValue}'.", + "ExactLengthValidator" => "'{PropertyName}' skal vera {MaxLength} teikn langt. Du har tasta inn {TotalLength} teikn.", + "InclusiveBetweenValidator" => "'{PropertyName}' skal vera mellom {From} og {To}. Du har tasta inn {PropertyValue}.", + "ExclusiveBetweenValidator" => "'{PropertyName}' skal vera mellom {From} og {To} (unntatt). Du har tasta inn {PropertyValue}.", + "CreditCardValidator" => "'{PropertyName}' er ikkje eit gyldig kredittkortnummer.", + "ScalePrecisionValidator" => "'{PropertyName}' kan ikkje vera meir enn {ExpectedPrecision} siffer totalt, inkludert {ExpectedScale} desimalar. {Digits} siffer og {ActualScale} desimalar vart funnen.", + "EmptyValidator" => "'{PropertyName}' skal vera tomt.", + "NullValidator" => "'{PropertyName}' skal vera tomt.", + "EnumValidator" => "'{PropertyName}' har ei rekkje verdiar men inneheld ikkje '{PropertyValue}'.", + // Additional fallback messages used by clientside validation integration. + "Length_Simple" => "'{PropertyName}' skal vera mellom {MinLength} og {MaxLength} teikn.", + "MinimumLength_Simple" => "'{PropertyName}' skal vera større enn eller lik {MinLength} teikn.", + "MaximumLength_Simple" => "'{PropertyName}' skal vera mindre enn eller lik {MaxLength} teikn.", + "ExactLength_Simple" => "'{PropertyName}' skal vera {MaxLength} teikn langt.", + "InclusiveBetween_Simple" => "'{PropertyName}' skal vera mellom {From} og {To}.", + _ => null, + }; +} diff --git a/src/FluentValidation/Resources/Languages/PolishLanguage.cs b/src/FluentValidation/Resources/Languages/PolishLanguage.cs index 302bb3fc3..731e51f4b 100644 --- a/src/FluentValidation/Resources/Languages/PolishLanguage.cs +++ b/src/FluentValidation/Resources/Languages/PolishLanguage.cs @@ -29,9 +29,9 @@ internal class PolishLanguage { "EmailValidator" => "Pole '{PropertyName}' nie zawiera poprawnego adresu email.", "GreaterThanOrEqualValidator" => "Wartość pola '{PropertyName}' musi być równa lub większa niż '{ComparisonValue}'.", "GreaterThanValidator" => "Wartość pola '{PropertyName}' musi być większa niż '{ComparisonValue}'.", - "LengthValidator" => "Długość pola '{PropertyName}' musi się zawierać pomiędzy {MinLength} i {MaxLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", + "LengthValidator" => "Długość pola '{PropertyName}' musi zawierać się pomiędzy {MinLength} i {MaxLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", "MinimumLengthValidator" => "Długość pola '{PropertyName}' musi być większa lub równa {MinLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", - "MaximumLengthValidator" => "Długość pola '{PropertyName}' musi być mniejszy lub równy {MaxLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", + "MaximumLengthValidator" => "Długość pola '{PropertyName}' musi być mniejsza lub równa {MaxLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", "LessThanOrEqualValidator" => "Wartość pola '{PropertyName}' musi być równa lub mniejsza niż '{ComparisonValue}'.", "LessThanValidator" => "Wartość pola '{PropertyName}' musi być mniejsza niż '{ComparisonValue}'.", "NotEmptyValidator" => "Pole '{PropertyName}' nie może być puste.", @@ -42,19 +42,19 @@ internal class PolishLanguage { "RegularExpressionValidator" => "'{PropertyName}' wprowadzono w niepoprawnym formacie.", "EqualValidator" => "Wartość pola '{PropertyName}' musi być równa '{ComparisonValue}'.", "ExactLengthValidator" => "Pole '{PropertyName}' musi posiadać długość {MaxLength} znaki(ów). Wprowadzono {TotalLength} znaki(ów).", - "InclusiveBetweenValidator" => "Wartość pola '{PropertyName}' musi się zawierać pomiędzy {From} i {To}. Wprowadzono {PropertyValue}.", - "ExclusiveBetweenValidator" => "Wartość pola '{PropertyName}' musi się zawierać pomiędzy {From} i {To} (wyłącznie). Wprowadzono {PropertyValue}.", - "CreditCardValidator" => "Pole '{PropertyName}' nie zawiera poprawnego numer karty kredytowej.", + "InclusiveBetweenValidator" => "Wartość pola '{PropertyName}' musi zawierać się pomiędzy {From} i {To}. Wprowadzono {PropertyValue}.", + "ExclusiveBetweenValidator" => "Wartość pola '{PropertyName}' musi zawierać się pomiędzy {From} i {To} (wyłącznie). Wprowadzono {PropertyValue}.", + "CreditCardValidator" => "Pole '{PropertyName}' nie zawiera poprawnego numeru karty kredytowej.", "ScalePrecisionValidator" => "Wartość pola '{PropertyName}' nie może mieć więcej niż {ExpectedPrecision} cyfr z dopuszczalną dokładnością {ExpectedScale} cyfr po przecinku. Znaleziono {Digits} cyfr i {ActualScale} cyfr po przecinku.", "EmptyValidator" => "Pole '{PropertyName}' musi być puste.", "NullValidator" => "Pole '{PropertyName}' musi być puste.", "EnumValidator" => "Pole '{PropertyName}' ma zakres wartości, który nie obejmuje {PropertyValue}.", // Additional fallback messages used by clientside validation integration. - "Length_Simple" => "Długość pola '{PropertyName}' musi się zawierać pomiędzy {MinLength} i {MaxLength} znaki(ów).", + "Length_Simple" => "Długość pola '{PropertyName}' musi zawierać się pomiędzy {MinLength} i {MaxLength} znaki(ów).", "MinimumLength_Simple" => "Długość pola '{PropertyName}' musi być większa lub równa {MinLength} znaki(ów).", - "MaximumLength_Simple" => "Długość pola '{PropertyName}' musi być mniejszy lub równy {MaxLength} znaki(ów).", + "MaximumLength_Simple" => "Długość pola '{PropertyName}' musi być mniejsza lub równa {MaxLength} znaki(ów).", "ExactLength_Simple" => "Pole '{PropertyName}' musi posiadać długość {MaxLength} znaki(ów).", - "InclusiveBetween_Simple" => "Wartość pola '{PropertyName}' musi się zawierać pomiędzy {From} i {To}.", + "InclusiveBetween_Simple" => "Wartość pola '{PropertyName}' musi zawierać się pomiędzy {From} i {To}.", _ => null, }; }