diff --git a/Changelog.txt b/Changelog.txt index 8e9690984..7502e3f73 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,10 @@ +12.0.0 - +Drops support for netstandard2.0, netstandard2.1 and .net 5. Minimum supported platform is now .net 6. +Add support for dependent rules for custom rules (#2170) +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) diff --git a/docs/cascade.md b/docs/cascade.md index d71523dbb..ae1a86046 100644 --- a/docs/cascade.md +++ b/docs/cascade.md @@ -25,11 +25,6 @@ The two cascade modes are: - `Continue` (the default) - always invokes all rules in a validator class, or all validators in a rule, depending on where it is used (see below). - `Stop` - stops executing a validator class as soon as a rule fails, or stops executing a rule as soon as a validator fails, depending on where it is used (see below). -```eval_rst -.. warning:: - The Stop option is only available in FluentValidation 9.1 and newer. In older versions, you can use StopOnFirstFailure instead (see "Stop vs StopOnFirstFailure"). -``` - If you have a validator class with multiple rules, and would like this `Stop` behaviour to be set for all of your rules, you could do e.g.: ```csharp RuleFor(x => x.Forename).Cascade(CascadeMode.Stop).NotNull().NotEqual("foo"); @@ -60,45 +55,19 @@ To set the default cascade modes at rule-level and/or validator class-level glob ```eval_rst .. warning:: - The RuleLevelCascadeMode, ClassLevelCascadeMode, and their global defaults are only available in FluentValidation 11 and newer. See below. + The RuleLevelCascadeMode, ClassLevelCascadeMode, and their global defaults are only available in FluentValidation 11 and newer. ``` -## Introduction of RuleLevelCascadeMode and ClassLevelCascadeMode (and deprecation of CascadeMode) +## Introduction of RuleLevelCascadeMode and ClassLevelCascadeMode (and removal of CascadeMode) The `AbstractValidator.RuleLevelCascadeMode`, `AbstractValidator.ClassLevelCascadeMode`, and their global defaults were introduced in FluentValidation 11 -In older versions, there was only one property controlling cascade modes: `AbstractValidator.CascadeMode`. Changing this value would set the cascade mode at both validator class-level and rule-level. Therefore, for example, if you wanted to have the above-described functionality where you create a list of validation errors, by stopping on failure at rule-level to avoid crashes, but continuing at validator class-level, you would need to set `AbstractValidator.CascadeMode` to `Continue`, and then repeat `Cascade(CascadeMode.Stop)` on every rule chain (or use the deprecated `StopOnFirstFailure` with a warning; see "Stop vs StopOnFirstFailure"). +In older versions, there was only one property controlling cascade modes: `AbstractValidator.CascadeMode`. Changing this value would set the cascade mode at both validator class-level and rule-level. Therefore, for example, if you wanted to have the above-described functionality where you create a list of validation errors, by stopping on failure at rule-level to avoid crashes, but continuing at validator class-level, you would need to set `AbstractValidator.CascadeMode` to `Continue`, and then repeat `Cascade(CascadeMode.Stop)` on every rule chain. The new properties enable finer control of the cascade mode at the different levels, with less repetition. ```eval_rst .. warning:: - In FluentValidation11, there are _no_ breaking changes to cascade modes. The `AbstractValidator.CascadeMode` property (and its global default property) are still present, and they function exactly the same as they did before, but they do so by setting / returning the values of `RuleLevelCascadeMode` and `ClassLevelCascadeMode`, as opposed to being used by the code directly. - - However, `AbstractValidator.CascadeMode` and its global default are deprecated, and will be removed in a future version. To convert to the new properties, see `the upgrade guide `_. -``` - -## Stop vs StopOnFirstFailure - -In FluentValidation 9.0 and older, the `CascadeMode.StopOnFirstFailure` option was used to provide control over the default cascade mode at rule-level, but its use was not intuitive. There was no `CascadeMode.Stop` option. - -With `StopOnFirstFailure`, the following would provide the example behavior described previously (stop any rule if it fails, but then continue executing at validator class-level, so that all rules are executed): - -```csharp -CascadeMode = CascadeMode.StopOnFirstFailure; - -RuleFor(x => x.Forename).NotNull().NotEqual("foo"); -RuleFor(x => x.MiddleNames).NotNull().NotEqual("foo"); -RuleFor(x => x.Surname).NotNull().NotEqual("foo"); + The `CascadeMode` property was deprecated in FluentValidation 11 and removed in FluentValidation 12. The `RuleLevelCascadeMode` and `ClassLevelCascadeMode` properties should be used instead. + + To convert to the new properties, see `the upgrade guide `_. ``` -If they all fail, you will get three validation errors. That is the equivalent of doing - -```csharp -RuleFor(x => x.Forename).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo"); -RuleFor(x => x.MiddleNames).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo"); -RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo"); -``` -This behaviour caused a lot of confusion over the years, so the `Stop` option was introduced in FluentValidation 9.1. Using `Stop` instead of `StopOnFirstFailure`, _any_ failure at all would stop execution, so only the first failure result would be returned. - -The `Stop` option was introduced rather than changing the behaviour of `StopOnFirstFailure` as this would've been a very subtle breaking change, so we thought it was best to maintain the existing behaviour while adding a new option. `StopOnFirstFailure` was marked as Obsolete in FluentValidation 9.1, and generated compiler warnings. - -See also the FluentValidation 11 changes described above, that further change how the above code would be written. `StopOnFirstFailure` is still available as of version 11 and has no breaking changes to how it functions. However, in a future version, it _will_ be removed, now that it's possible to replicate its behavior using the new properties `AbstractValidator.RuleLevelCascadeMode` and `AbstractValidator.ClassLevelCascadeMode`. diff --git a/docs/index.rst b/docs/index.rst index 7e24e65d2..41a3b5721 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,16 +8,11 @@ FluentValidation FluentValidation is a .NET library for building strongly-typed validation rules. -FluentValidation 11 supports the following platforms: +FluentValidation 12 supports the following platforms: -* .NET Core 3.1 -* .NET 5 * .NET 6 * .NET 7 * .NET 8 -* `.NET Standard 2.0 `_ - -For automatic validation with ASP.NET, FluentValidation supports ASP.NET running on .NET Core 3.1, .NET 5 or .NET 6. If you're new to using FluentValidation, check out the :doc:`start` page. @@ -127,7 +122,8 @@ Example :maxdepth: 1 :caption: Upgrading + upgrading-to-12 upgrading-to-11 upgrading-to-10 upgrading-to-9 - upgrading-to-8 \ No newline at end of file + upgrading-to-8 diff --git a/docs/upgrading-to-11.md b/docs/upgrading-to-11.md index 1e80cde11..e4af720e6 100644 --- a/docs/upgrading-to-11.md +++ b/docs/upgrading-to-11.md @@ -85,7 +85,7 @@ RuleLevelCascadeMode = CascadeMode.Stop; ...or their global default equivalents. - See [this page in the documentation](https://docs.fluentvalidation.net/en/latest/conditions.html#setting-the-cascade-mode) for details of how cascade modes work and the reasons for this change. + See [this page in the documentation](https://docs.fluentvalidation.net/en/latest/conditions.html#setting-the-cascade-mode) for details of how cascade modes work. As `StopOnFirstFailure` is deprecated and scheduled for removal, it cannot be assigned to either of the two new `AbstractValidator` properties or their global equivalents (it still can be assigned to the also-deprecated `AbstractValidator.CascadeMode`). Attempting to set the new properties to `StopOnFirstFailure` will simply result in `Stop` being used instead. diff --git a/docs/upgrading-to-12.md b/docs/upgrading-to-12.md new file mode 100644 index 000000000..0cc96c0bd --- /dev/null +++ b/docs/upgrading-to-12.md @@ -0,0 +1,94 @@ +# 12.0 Upgrade Guide + +### Introduction + +FluentValidation 12.0 is a major release that included several breaking changes. Please review this document carefully before upgrading from FluentValidation 11.x to 12. + +The main goal of this release was removal of deprecated code and removal of support for obsolete platforms. There are no new features in this release. + +### Changes in supported platforms + +- .NET 5 is no longer supported (Microsoft's support for .NET 5 ended in November 2022) +- .NET Core 3.1 is no longer supported (Microsoft's support for .NET Core 3.1 ended in December 2022) +- .NET Standard is no longer support +- .NET 6 is the minimum supported version + +If you still need .NET Standard 2.0 compatibility then you will need to continue to use FluentValidation 11.x and only upgrade to FluentValidation 12 once you've moved to a more modern version of .NET. + +### Removal of the Transform and TransformForEach methods + +The `Transform` and `TransformForEach` methods deprecated in 11.x have been removed. For details on how to migrate see [https://github.com/FluentValidation/FluentValidation/issues/2072](https://github.com/FluentValidation/FluentValidation/issues/2072) + +### Removal of CascadeMode.StopOnFirstFailure + +The `StopOnFirstFailure` cascade option was deprecated in FluentValidation 11.0 and has now been removed, along with the `AbstractValidator.CascadeMode` and `ValidatorOptions.Global.CascadeMode` properties which were also deprecated in 11.0. + +If were previously setting `ValidatorOptions.Global.CascadeMode` to `Continue` or `Stop`, you can simply replace this with the following: + +```csharp +ValidatorOptions.Global.DefaultClassLevelCascadeMode = CascadeMode.; +ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.; +``` + +If you were previously setting it to `StopOnFirstFailure`, replace it with the following: + +```csharp +ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; +``` + +Similarly, if you were previously setting `AbstractValidator.CascadeMode` to `Continue` or `Stop`, replace this with the following: + +```csharp +ClassLevelCascadeMode = CascadeMode.; +RuleLevelCascadeMode = CascadeMode.; +``` + +If you were previously setting it to `StopOnFirstFailure`, replace it with the following: + +```csharp +ClassLevelCascadeMode = CascadeMode.Continue; +RuleLevelCascadeMode = CascadeMode.Stop; +``` + +If you were calling `.Cascade(CascadeMode.StopOnFirstFailure)` in a rule chain, replace `StopOnFirstFailure` with `Stop`. + +### Removal of InjectValidator and related methods + +The `InjectValidator` method was deprecated in 11.x and removed in 12.0. + +This method allowed you to implicitly inject a child validator from the ASP.NET Service Provider: + +```csharp +public class PersonValidator : AbstractValidator +{ + public PersonValidator() + { + RuleFor(x => x.Address).InjectValidator(); + } +} +``` + +Assuming that the address property is of type `Address`, the above code would attempt to resolve an `IValidator
` and use this to validator the `Address` property. This method can only be used when working with ASP.NET MVC's auto-validation feature and cannot be used in other contexts. + +Instead of using `InjectValidator`, you should instead use a more traditional constructor injection approach, which is not just limited to ASP.NET MVC: + +```csharp +public class PersonValidator : AbstractValidator +{ + public PersonValidator(IValidator
addressValidator) + { + RuleFor(x => x.Address).SetValidator(addressValidator); + } +} +``` + +### Removal of AbstractValidator.EnsureInstanceNotNull + +In previous versions of FluentValidation it was possible to override the `AbstractValidator.EnsureInstanceNotNull` method to disable FluentValidation's root-model null check. The ability to do this was deprecated in 11.5.x and has now been removed. For further details please see [https://github.com/FluentValidation/FluentValidation/issues/2069](https://github.com/FluentValidation/FluentValidation/issues/2069) + + +### Other breaking API changes + +- The `ITestValidationContinuation` interface now exposes a `MatchedFailures` property (as well as the existing `UnmatchedFailures`) +- The `ShouldHaveAnyValidationError` method has been renamed to `ShouldHaveValidationErrors` +- `ShouldNotHaveAnyValidationErrors` and `ShouldHaveValidationErrors` are now instance methods on `TestValidationResult`, instead of extension methods. diff --git a/src/CommonAssemblyInfo.cs b/src/CommonAssemblyInfo.cs index a67267b9f..882a984f6 100644 --- a/src/CommonAssemblyInfo.cs +++ b/src/CommonAssemblyInfo.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; -[assembly : AssemblyVersion("11.0.0.0")] +[assembly : AssemblyVersion("12.0.0.0")] diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7fdb62a9b..8cd67bb79 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 11.9.1 - + 12.0.0 + preview1 Jeremy Skinner @@ -17,7 +17,7 @@ en $(MSBuildProjectDirectory)/../FluentValidation-Release.snk $(MSBuildProjectDirectory)/../../.build/packages - 11.0.0 + 12.0.0 diff --git a/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs b/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs deleted file mode 100644 index 202573ae2..000000000 --- a/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -#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 - -using FluentValidation.Internal; - -namespace FluentValidation; - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using Validators; - -/// -/// Extension methods for working with a Service Provider. -/// -public static class DependencyInjectionExtensions { - - /// - /// Gets the service provider associated with the validation context. - /// - /// - /// - /// - [Obsolete("Storing a service provider within a ValidationContext is no longer supported. If you still require this functionality, you should manually store the service provider within context.RootContextData")] - public static IServiceProvider GetServiceProvider(this IValidationContext context) - => Get(context.RootContextData); - - /// - /// Gets the service provider associated with the validation context. - /// - /// - /// - /// - [Obsolete("Storing a service provider within a ValidationContext is no longer supported. If you still require this functionality, you should manually store the service provider within context.RootContextData")] - public static IServiceProvider GetServiceProvider(this MessageBuilderContext context) - => Get(context.ParentContext.RootContextData); - - private static IServiceProvider Get(IDictionary rootContextData) { - if (rootContextData.TryGetValue("_FV_ServiceProvider", out var sp)) { - if (sp is IServiceProvider serviceProvider) { - return serviceProvider; - } - } - - throw new InvalidOperationException("The service provider has not been configured to work with FluentValidation. Making use of InjectValidator or GetServiceProvider is only supported when using the automatic MVC integration."); - } - - /// - /// Sets the service provider associated with the validation context. - /// - /// - /// - [Obsolete("Storing a service provider within a ValidationContext is no longer supported. If you still require this functionality, you should manually store the service provider within context.RootContextData")] - public static void SetServiceProvider(this IValidationContext context, IServiceProvider serviceProvider) { - context.RootContextData["_FV_ServiceProvider"] = serviceProvider; - } - - /// - /// Uses the Service Provider to inject the default validator for the property type. - /// - /// - /// - /// - /// - /// - [Obsolete("The InjectValidator method is deprecated and will be removed in a future release. Please use Constructor Injection instead. See the following page for further details: https://github.com/FluentValidation/FluentValidation/issues/1960")] - public static IRuleBuilderOptions InjectValidator(this IRuleBuilder ruleBuilder, params string[] ruleSets) { - return ruleBuilder.InjectValidator((s, ctx) => s.GetService().GetValidator(), ruleSets); - } - - /// - /// Uses the Service Provider to inject the default validator for the property type. - /// - /// - /// - /// - /// - /// - /// - [Obsolete("The InjectValidator method is deprecated and will be removed in a future release. Please use Constructor Injection instead. See the following page for further details: https://github.com/FluentValidation/FluentValidation/issues/1960")] - public static IRuleBuilderOptions InjectValidator(this IRuleBuilder ruleBuilder, Func, IValidator> callback, params string[] ruleSets) { - var adaptor = new ChildValidatorAdaptor((context, _) => { - var serviceProvider = context.GetServiceProvider(); - var validator = callback(serviceProvider, context); - return validator; - }, typeof(IValidator)); - - adaptor.RuleSets = ruleSets; - return ruleBuilder.SetAsyncValidator(adaptor); - } -} diff --git a/src/FluentValidation.DependencyInjectionExtensions/FluentValidation.DependencyInjectionExtensions.csproj b/src/FluentValidation.DependencyInjectionExtensions/FluentValidation.DependencyInjectionExtensions.csproj index cf37f3b49..31fa07b90 100644 --- a/src/FluentValidation.DependencyInjectionExtensions/FluentValidation.DependencyInjectionExtensions.csproj +++ b/src/FluentValidation.DependencyInjectionExtensions/FluentValidation.DependencyInjectionExtensions.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1 + net6.0 FluentValidation.DependencyInjectionExtensions FluentValidation.DependencyInjectionExtensions FluentValidation.DependencyInjectionExtensions @@ -18,10 +18,11 @@ true true true + false - + diff --git a/src/FluentValidation.Tests.Benchmarks/AccessorCacheBenchmark.cs b/src/FluentValidation.Tests.Benchmarks/AccessorCacheBenchmark.cs new file mode 100644 index 000000000..b3bf5c157 --- /dev/null +++ b/src/FluentValidation.Tests.Benchmarks/AccessorCacheBenchmark.cs @@ -0,0 +1,58 @@ +#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 + +namespace FluentValidation.Tests.Benchmarks { + using System; + using System.Linq.Expressions; + using System.Reflection; + using BenchmarkDotNet.Attributes; + using Internal; + + [MemoryDiagnoser] + public class AccessorCacheBenchmark { + + private Expression> Expression { get; set; } + private MemberInfo Member { get; set; } + + [GlobalSetup] + public void GlobalSetup() { + Expression = GetExpression(x => x.Property); + Member = Expression.GetMember(); + } + + [Benchmark] + public Func GetCachedAccessor() { + return AccessorCache.GetCachedAccessor(Member, Expression, false, ""); + } + + [Benchmark] + public Func GetCachedAccessorWithCachePrefix() { + return AccessorCache.GetCachedAccessor(Member, Expression, false, "Prefix"); + } + + private Expression> GetExpression(Expression> expression) { + return expression; + } + + public class TestModel { + public int Property { get; set; } + } + } +} diff --git a/src/FluentValidation.Tests/CascadeModePropertiesTesterLegacy.cs b/src/FluentValidation.Tests/CascadeModePropertiesTesterLegacy.cs deleted file mode 100644 index d2058601a..000000000 --- a/src/FluentValidation.Tests/CascadeModePropertiesTesterLegacy.cs +++ /dev/null @@ -1,187 +0,0 @@ -#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 -#pragma warning disable 1998 - -namespace FluentValidation.Tests; - -using System; -using Xunit; - -public class CascadeModePropertiesTesterLegacy : IDisposable { - TestValidator _validator; - - public CascadeModePropertiesTesterLegacy() { - SetBothGlobalCascadeModes(CascadeMode.Continue); - _validator = new TestValidator(); - } - - public void Dispose() { - SetBothGlobalCascadeModes(CascadeMode.Continue); - } - - [Fact] - public void Setting_global_default_CascadeMode_Stop_sets_both_rule_and_class_level_global_default_properties() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.DefaultRuleLevelCascadeMode); - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.DefaultClassLevelCascadeMode); - } - - [Fact] - public void Setting_global_default_CascadeMode_Continue_sets_both_rule_and_class_level_global_default_properties() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - - Assert.Equal(CascadeMode.Continue, ValidatorOptions.Global.DefaultRuleLevelCascadeMode); - Assert.Equal(CascadeMode.Continue, ValidatorOptions.Global.DefaultClassLevelCascadeMode); - } - - [Fact] - public void Setting_global_default_CascadeMode_StopOnFirstFailure_sets_rule_Stop_and_class_Continue() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.DefaultRuleLevelCascadeMode); - Assert.Equal(CascadeMode.Continue, ValidatorOptions.Global.DefaultClassLevelCascadeMode); - } - - [Fact] - public void Setting_class_CascadeMode_Stop_sets_both_rule_and_class_level_properties() { - _validator.CascadeMode = CascadeMode.Stop; - - Assert.Equal(CascadeMode.Stop, _validator.RuleLevelCascadeMode); - Assert.Equal(CascadeMode.Stop, _validator.ClassLevelCascadeMode); - } - - [Fact] - public void Setting_class_CascadeMode_Continue_sets_both_rule_and_class_level_properties() { - _validator.CascadeMode = CascadeMode.Stop; - _validator.CascadeMode = CascadeMode.Continue; - - Assert.Equal(CascadeMode.Continue, _validator.RuleLevelCascadeMode); - Assert.Equal(CascadeMode.Continue, _validator.ClassLevelCascadeMode); - } - - [Fact] - public void Setting_class_CascadeMode_StopOnFirstFailure_sets_rule_Stop_and_class_Continue() { - _validator.CascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, _validator.RuleLevelCascadeMode); - Assert.Equal(CascadeMode.Continue, _validator.ClassLevelCascadeMode); - } - - [Fact] - public void Setting_global_DefaultRuleLevelCascadeMode_to_StopOnFirstFailure_sets_Stop() { - ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.DefaultRuleLevelCascadeMode); - } - - [Fact] - public void Setting_global_DefaultClassLevelCascadeMode_to_StopOnFirstFailure_sets_Stop() { - ValidatorOptions.Global.DefaultClassLevelCascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.DefaultClassLevelCascadeMode); - } - - [Fact] - public void Setting_class_RuleLevelCascadeMode_to_StopOnFirstFailure_sets_Stop() { - _validator.RuleLevelCascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, _validator.RuleLevelCascadeMode); - } - - [Fact] - public void Setting_class_ClassLevelCascadeMode_to_StopOnFirstFailure_sets_Stop() { - _validator.ClassLevelCascadeMode = CascadeMode.StopOnFirstFailure; - - Assert.Equal(CascadeMode.Stop, _validator.ClassLevelCascadeMode); - } - - [Fact] - public void Global_default_CascadeMode_Get_returns_Stop_when_both_Stop() { - SetBothGlobalCascadeModes(CascadeMode.Stop); - - Assert.Equal(CascadeMode.Stop, ValidatorOptions.Global.CascadeMode); - } - - [Fact] - public void Global_default_CascadeMode_Get_returns_Continue_when_both_Continue() { - SetBothGlobalCascadeModes(CascadeMode.Stop); - SetBothGlobalCascadeModes(CascadeMode.Continue); - - Assert.Equal(CascadeMode.Continue, ValidatorOptions.Global.CascadeMode); - } - - [Fact] - public void Global_default_CascadeMode_Get_returns_StopOnFirstFailure_when_class_Continue_and_rule_Stop() { - ValidatorOptions.Global.DefaultClassLevelCascadeMode = CascadeMode.Continue; - ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; - - Assert.Equal(CascadeMode.StopOnFirstFailure, ValidatorOptions.Global.CascadeMode); - } - - [Fact] - public void Global_default_CascadeMode_Get_throws_exception_when_class_Stop_and_rule_Continue() { - ValidatorOptions.Global.DefaultClassLevelCascadeMode = CascadeMode.Stop; - ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Continue; - - Assert.ThrowsAny(() => ValidatorOptions.Global.CascadeMode); - } - - [Fact] - public void Class_CascadeMode_Get_returns_Stop_when_both_Stop() { - SetBothValidatorCascadeModes(CascadeMode.Stop); - - Assert.Equal(CascadeMode.Stop, _validator.CascadeMode); - } - - [Fact] - public void Class_CascadeMode_Get_returns_Continue_when_both_Continue() { - SetBothValidatorCascadeModes(CascadeMode.Stop); - SetBothValidatorCascadeModes(CascadeMode.Continue); - - Assert.Equal(CascadeMode.Continue, _validator.CascadeMode); - } - - [Fact] - public void Class_CascadeMode_Get_returns_StopOnFirstFailure_when_class_Continue_and_rule_Stop() { - _validator.ClassLevelCascadeMode = CascadeMode.Continue; - _validator.RuleLevelCascadeMode = CascadeMode.Stop; - - Assert.Equal(CascadeMode.StopOnFirstFailure, _validator.CascadeMode); - } - - [Fact] - public void Class_CascadeMode_Get_throws_exception_when_class_Stop_and_rule_Continue() { - _validator.ClassLevelCascadeMode = CascadeMode.Stop; - _validator.RuleLevelCascadeMode = CascadeMode.Continue; - - Assert.ThrowsAny(() => _validator.CascadeMode); - } - - private void SetBothValidatorCascadeModes(CascadeMode cascadeMode) { - _validator.ClassLevelCascadeMode = cascadeMode; - _validator.RuleLevelCascadeMode = cascadeMode; - } - - private static void SetBothGlobalCascadeModes(CascadeMode cascadeMode) { - ValidatorOptions.Global.DefaultClassLevelCascadeMode = cascadeMode; - ValidatorOptions.Global.DefaultRuleLevelCascadeMode = cascadeMode; - } -} diff --git a/src/FluentValidation.Tests/CascadingFailuresTester.cs b/src/FluentValidation.Tests/CascadingFailuresTester.cs index 6de6739fd..de3ebf90a 100644 --- a/src/FluentValidation.Tests/CascadingFailuresTester.cs +++ b/src/FluentValidation.Tests/CascadingFailuresTester.cs @@ -118,14 +118,6 @@ public class CascadingFailuresTester : IDisposable { results.Errors.Count.ShouldEqual(1); } - [Fact] - public void Validation_stops_on_first_failure_when_globaldefault_both_Continue_and_ruleleveloverride_Stop_legacy() { - SetBothGlobalCascadeModes(CascadeMode.Continue); - _validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = _validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - [Fact] public void Validation_continues_to_second_validator_when_first_validator_succeeds_and_globaldefault_both_Stop() { SetBothGlobalCascadeModes(CascadeMode.Stop); @@ -217,15 +209,6 @@ public class CascadingFailuresTester : IDisposable { results.Errors.Count.ShouldEqual(1); } - [Fact] - public void Validation_stops_on_failure_when_classlevel_Continue_and_ruleleveldefault_Continue_and_ruleleveloverride_Stop_legacy() { - SetBothValidatorCascadeModes(CascadeMode.Continue); - - _validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = _validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - [Fact] public void Cascade_mode_can_be_set_after_validator_instantiated() { _validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); @@ -322,15 +305,6 @@ public class CascadingFailuresTester : IDisposable { results.Errors.Count.ShouldEqual(1); } - [Fact] - public async Task Validation_stops_on_first_Failure_when_globaldefault_both_Continue_and_ruleleveloverride_Stop_async_legacy() { - SetBothGlobalCascadeModes(CascadeMode.Continue); - _validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await _validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] public async Task Validation_stops_on_first_Failure_when_globaldefault_both_Continue_and_ruleleveloverride_Stop_async_and_async_validator_is_invoked_synchronously() { SetBothGlobalCascadeModes(CascadeMode.Continue); @@ -339,14 +313,6 @@ public class CascadingFailuresTester : IDisposable { results.Errors.Count.ShouldEqual(1); } - [Fact] - public async Task Validation_stops_on_first_Failure_when_globaldefault_both_Continue_and_ruleleveloverride_Stop_async_and_async_validator_is_invoked_synchronously_legacy() { - SetBothGlobalCascadeModes(CascadeMode.Continue); - _validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = await _validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - [Fact] public async Task Validation_continues_to_second_validator_when_first_validator_succeeds_and_globaldefault_both_Stop_async() { SetBothGlobalCascadeModes(CascadeMode.Stop); @@ -466,15 +432,6 @@ public class CascadingFailuresTester : IDisposable { results.Errors.Count.ShouldEqual(1); } - [Fact] - public async Task Validation_stops_on_failure_when_classlevel_Continue_and_ruleleveldefault_Continue_and_ruleleveloverride_Stop_async_legacy() { - SetBothValidatorCascadeModes(CascadeMode.Continue); - - _validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await _validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - [Fact] public async Task Cascade_mode_can_be_set_after_validator_instantiated_async() { _validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); @@ -519,6 +476,14 @@ public class CascadingFailuresTester : IDisposable { 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. + // For compatibility, "Stop" should still equate to 2, rather than being renumbered to 1. + Assert.Equal(0, (int)CascadeMode.Continue); + Assert.Equal(2, (int)CascadeMode.Stop); + } + private void SetBothValidatorCascadeModes(CascadeMode cascadeMode) { _validator.ClassLevelCascadeMode = cascadeMode; _validator.RuleLevelCascadeMode = cascadeMode; diff --git a/src/FluentValidation.Tests/CascadingFailuresTesterLegacy.cs b/src/FluentValidation.Tests/CascadingFailuresTesterLegacy.cs deleted file mode 100644 index 241b461c6..000000000 --- a/src/FluentValidation.Tests/CascadingFailuresTesterLegacy.cs +++ /dev/null @@ -1,364 +0,0 @@ -#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 -#pragma warning disable 1998 - -namespace FluentValidation.Tests; - -using System; -using System.Threading.Tasks; -using Xunit; - -public class CascadingFailuresTesterLegacy : IDisposable { - TestValidator validator; - - public CascadingFailuresTesterLegacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator = new TestValidator(); - } - - public void Dispose() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - } - - [Fact] - public void Validation_continues_on_failure() { - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_stops_on_first_failure() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_stops_on_first_failure_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_continues_on_failure_when_set_to_Stop_globally_and_overriden_at_rule_level() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_continues_on_failure_when_set_to_Stop_globally_and_overriden_at_rule_level_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_continues_to_second_validator_when_first_validator_succeeds_and_cascade_set_to_stop() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - validator.RuleFor(x => x.Surname).NotNull().Length(2, 10); - var result = validator.Validate(new Person() {Surname = "x"}); - result.IsValid.ShouldBeFalse(); - } - - [Fact] - public void Validation_continues_to_second_validator_when_first_validator_succeeds_and_cascade_set_to_stop_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - validator.RuleFor(x => x.Surname).NotNull().Length(2, 10); - var result = validator.Validate(new Person() {Surname = "x"}); - result.IsValid.ShouldBeFalse(); - } - - [Fact] - public void Validation_stops_on_first_failure_when_set_to_StopOnFirstFailure_at_validator_level() { - validator.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_stops_on_first_failure_when_set_to_StopOnFirstFailure_at_validator_level_legacy() { - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_continues_when_set_to_Continue_at_validator_level() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_continues_on_failure_when_set_to_Stop_at_validator_level_and_overriden_at_rule_level() { - validator.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_continues_on_failure_when_set_to_StopOnFirstFailure_at_validator_level_and_overriden_at_rule_level_legacy() { - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public void Validation_stops_on_failure_when_set_to_Continue_and_overriden_at_rule_level() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Validation_stops_on_failure_when_set_to_Continue_and_overriden_at_rule_level_legacy() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Cascade_mode_can_be_set_after_validator_instantiated() { - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - validator.CascadeMode = CascadeMode.Stop; - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Cascade_mode_can_be_set_after_validator_instantiated_legacy() { - validator.RuleFor(x => x.Surname).NotNull().Equal("Foo"); - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - var results = validator.Validate(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_continues_on_failure_async() { - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_stops_on_first_failure_async() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_stops_on_first_failure_async_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_continues_on_failure_when_set_to_Stop_globally_and_overriden_at_rule_level_async() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_continues_on_failure_when_set_to_Stop_globally_and_overriden_at_rule_level_async_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level_async() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level_async_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - - [Fact] - public async Task Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level_and_async_validator_is_invoked_synchronously() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).NotNull().Equal("Foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_stops_on_first_Failure_when_set_to_Continue_globally_and_overriden_at_rule_level_and_async_validator_is_invoked_synchronously_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Continue; - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Equal("Foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_continues_to_second_validator_when_first_validator_succeeds_and_cascade_set_to_stop_async() { - ValidatorOptions.Global.CascadeMode = CascadeMode.Stop; - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var result = await validator.ValidateAsync(new Person {Surname = "x"}); - result.IsValid.ShouldBeFalse(); - } - - [Fact] - public async Task Validation_continues_to_second_validator_when_first_validator_succeeds_and_cascade_set_to_stop_async_legacy() { - ValidatorOptions.Global.CascadeMode = CascadeMode.StopOnFirstFailure; - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var result = await validator.ValidateAsync(new Person {Surname = "x"}); - result.IsValid.ShouldBeFalse(); - } - - [Fact] - public async Task Validation_stops_on_first_failure_when_set_to_StopOnFirstFailure_at_validator_level_async() { - validator.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] public async Task Validation_stops_on_first_failure_when_set_to_StopOnFirstFailure_at_validator_level_async_legacy() { - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_continues_when_set_to_Continue_at_validator_level_async() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_continues_on_failure_when_set_to_StopOnFirstFailure_at_validator_level_and_overriden_at_rule_level_async() { - validator.CascadeMode = CascadeMode.Stop; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_continues_on_failure_when_set_to_StopOnFirstFailure_at_validator_level_and_overriden_at_rule_level_async_legacy() { - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Continue).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(2); - } - - [Fact] - public async Task Validation_stops_on_failure_when_set_to_Continue_and_overriden_at_rule_level_async() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Validation_stops_on_failure_when_set_to_Continue_and_overriden_at_rule_level_async_legacy() { - validator.CascadeMode = CascadeMode.Continue; - - validator.RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Cascade_mode_can_be_set_after_validator_instantiated_async() { - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - validator.CascadeMode = CascadeMode.Stop; - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Cascade_mode_can_be_set_after_validator_instantiated_async_legacy() { - validator.RuleFor(x => x.Surname).MustAsync(async (x, c) => x != null).MustAsync(async (x, c) => x == "foo"); - validator.CascadeMode = CascadeMode.StopOnFirstFailure; - var results = await validator.ValidateAsync(new Person()); - results.Errors.Count.ShouldEqual(1); - } -} diff --git a/src/FluentValidation.Tests/ChainedValidationTester.cs b/src/FluentValidation.Tests/ChainedValidationTester.cs index 3c584af9a..135efcfa6 100644 --- a/src/FluentValidation.Tests/ChainedValidationTester.cs +++ b/src/FluentValidation.Tests/ChainedValidationTester.cs @@ -177,9 +177,7 @@ public class ChainedValidationTester { public class DepartmentValidator : AbstractValidator { public DepartmentValidator() { -#pragma warning disable 618 - CascadeMode = CascadeMode.StopOnFirstFailure; -#pragma warning restore 618 + RuleLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.Manager).NotNull(); RuleFor(x => x.Assistant.Surname).NotEqual(x => x.Manager.Surname).When(x => x.Assistant != null && x.Manager.Surname != null); } diff --git a/src/FluentValidation.Tests/ConditionTests.cs b/src/FluentValidation.Tests/ConditionTests.cs index 044f0bc0a..00aac28cd 100644 --- a/src/FluentValidation.Tests/ConditionTests.cs +++ b/src/FluentValidation.Tests/ConditionTests.cs @@ -211,6 +211,47 @@ public class ConditionTests { result.IsValid.ShouldBeFalse(); } + [Fact] + public void Can_try_access_property_value_in_custom_condition() { + var validator = new TestValidator(); + validator + .RuleFor(x => x.Surname) + .Must(_ => false) + .Configure(cfg => { + cfg.ApplyCondition(context => { + cfg.TryGetPropertyValue(context.InstanceToValidate, out var value); + return value != null; + }); + }); + + var result = validator.Validate(new Person()); + result.IsValid.ShouldBeTrue(); + + result = validator.Validate(new Person {Surname = "foo"}); + result.IsValid.ShouldBeFalse(); + } + + [Fact] + public void Can_try_access_property_value_in_custom_condition_foreach() { + var validator = new TestValidator(); + validator + .RuleForEach(x => x.Children) + .Must(_ => false) + .Configure(cfg => { + cfg.ApplyCondition(context => { + cfg.TryGetPropertyValue>(context.InstanceToValidate, out var value); + + return value != null; + }); + }); + + var result = validator.Validate(new Person()); + result.IsValid.ShouldBeTrue(); + + result = validator.Validate(new Person { Children = new List { new() }}); + result.IsValid.ShouldBeFalse(); + } + private class TestConditionValidator : AbstractValidator { public TestConditionValidator() { RuleFor(x => x.Forename).NotNull().When(x => x.Id == 0); diff --git a/src/FluentValidation.Tests/DefaultValidatorExtensionTester.cs b/src/FluentValidation.Tests/DefaultValidatorExtensionTester.cs index db043eb3a..c0870fe4f 100644 --- a/src/FluentValidation.Tests/DefaultValidatorExtensionTester.cs +++ b/src/FluentValidation.Tests/DefaultValidatorExtensionTester.cs @@ -215,13 +215,13 @@ public class DefaultValidatorExtensionTester { [Fact] public void ScalePrecision_should_create_ScalePrecisionValidator() { validator.RuleFor(x => x.Discount).PrecisionScale(5, 2, false); - AssertValidator>(); + AssertValidator>(); } [Fact] public void ScalePrecision_should_create_ScalePrecisionValidator_with_ignore_trailing_zeros() { validator.RuleFor(x => x.Discount).PrecisionScale(5, 2, true); - AssertValidator>(); + AssertValidator>(); } [Fact] diff --git a/src/FluentValidation.Tests/ForEachRuleTests.cs b/src/FluentValidation.Tests/ForEachRuleTests.cs index e3e95e0dc..fab4538fc 100644 --- a/src/FluentValidation.Tests/ForEachRuleTests.cs +++ b/src/FluentValidation.Tests/ForEachRuleTests.cs @@ -220,12 +220,10 @@ await ExclusiveDelay(1) [Fact] public void Can_use_cascade_with_RuleForEach() { var validator = new InlineValidator(); -#pragma warning disable 618 validator.RuleForEach(x => x.NickNames) - .Cascade(CascadeMode.StopOnFirstFailure) + .Cascade(CascadeMode.Stop) .NotNull() .NotEqual("foo"); -#pragma warning restore 618 var result = validator.Validate(new Person {NickNames = new string[] {null}}); result.Errors.Count.ShouldEqual(1); diff --git a/src/FluentValidation.Tests/RuleDependencyTests.cs b/src/FluentValidation.Tests/RuleDependencyTests.cs index fc216051b..9b0acf463 100644 --- a/src/FluentValidation.Tests/RuleDependencyTests.cs +++ b/src/FluentValidation.Tests/RuleDependencyTests.cs @@ -38,8 +38,7 @@ public class RuleDependencyTests { } [Fact] - public void Does_not_invoke_dependent_rule_if_parent_rule_does_not_pass() - { + public void Does_not_invoke_dependent_rule_if_parent_rule_does_not_pass() { var validator = new TestValidator(); validator.RuleFor(x => x.Surname).NotNull() .DependentRules(() => @@ -253,8 +252,7 @@ public void Nested_dependent_rules_inside_ruleset_no_result_when_second_level_fa [Fact] - public void Nested_dependent_rules_inside_ruleset_inside_method() - { + public void Nested_dependent_rules_inside_ruleset_inside_method() { var validator = new TestValidator(); validator.RuleSet("MyRuleSet", () => { @@ -277,8 +275,36 @@ public void Nested_dependent_rules_inside_ruleset_inside_method() results.Errors.Single().PropertyName.ShouldEqual("Address"); } - private void BaseValidation(InlineValidator validator) - { + [Fact] + public void Invokes_dependent_rule_if_parent_custom_rule_passes() { + var validator = new TestValidator(); + validator.RuleFor(x => x.Surname).Custom((name, context) => { }) + .DependentRules(() => { + validator.RuleFor(x => x.Forename).NotNull(); + }); + + var results = validator.Validate(new Person {Surname = "foo"}); + results.Errors.Count.ShouldEqual(1); + results.Errors.Single().PropertyName.ShouldEqual("Forename"); + } + + [Fact] + public void Does_not_invoke_dependent_rule_if_parent_custom_rule_does_not_pass() { + var validator = new TestValidator(); + validator.RuleFor(x => x.Surname).Custom((name, context) => { + context.AddFailure("Failed"); + }) + .DependentRules(() => + { + validator.RuleFor(x => x.Forename).NotNull(); + }); + + var results = validator.Validate(new Person { Surname = null }); + results.Errors.Count.ShouldEqual(1); + results.Errors.Single().PropertyName.ShouldEqual("Surname"); + } + + private void BaseValidation(InlineValidator validator) { validator.RuleFor(x => x.Address).NotNull(); } diff --git a/src/FluentValidation.Tests/SharedConditionTests.cs b/src/FluentValidation.Tests/SharedConditionTests.cs index a7990abc2..9fb519c4c 100644 --- a/src/FluentValidation.Tests/SharedConditionTests.cs +++ b/src/FluentValidation.Tests/SharedConditionTests.cs @@ -150,37 +150,7 @@ public SharedAsyncConditionInverseValidator() UnlessAsync(async (x,c) => x.Id == 0, () => { RuleFor(x => x.Forename).NotNull(); }); } } - - class BadValidatorDisablesNullCheck : AbstractValidator { - public BadValidatorDisablesNullCheck() { - When(x => x != null, () => { - RuleFor(x => x).Must(x => x != "foo"); - }); - } - - [Obsolete("Overriding the EnsureInstanceNotNull method to prevent FluentValidation for throwing an exception for null root models is no longer supported or recommended. The ability to override this method will be removed in FluentValidation 12. For details, see https://github.com/FluentValidation/FluentValidation/issues/2069")] - protected override void EnsureInstanceNotNull(object instanceToValidate) { - //bad. - } - } - - class AsyncBadValidatorDisablesNullCheck : AbstractValidator { - public AsyncBadValidatorDisablesNullCheck() { - When(x => x != null, () => { - RuleFor(x => x).Must(x => x != "foo"); - }); - - WhenAsync(async (x, ct) => x != null, () => { - RuleFor(x => x).Must(x => x != "foo"); - }); - } - - [Obsolete("Overriding the EnsureInstanceNotNull method to prevent FluentValidation for throwing an exception for null root models is no longer supported or recommended. The ability to override this method will be removed in FluentValidation 12. For details, see https://github.com/FluentValidation/FluentValidation/issues/2069")] - protected override void EnsureInstanceNotNull(object instanceToValidate) { - //bad. - } - } - + [Fact] public void shared_When_not_applied_to_grouped_collection_rules_when_initial_predicate_is_false() { var validator = new SharedCollectionConditionValidator(); @@ -792,20 +762,6 @@ public void Does_not_execute_customasync_Rule_when_condition_false() executions.ShouldEqual(2); } - [Fact] - public void Doesnt_throw_NullReferenceException_when_instance_not_null() { - var v = new BadValidatorDisablesNullCheck(); - var result = v.Validate((string) null); - result.IsValid.ShouldBeTrue(); - } - - [Fact] - public async Task Doesnt_throw_NullReferenceException_when_instance_not_null_async() { - var v = new AsyncBadValidatorDisablesNullCheck(); - var result = await v.ValidateAsync((string) null); - result.IsValid.ShouldBeTrue(); - } - [Fact] public void Shouldnt_break_with_hashcode_collision() { var v1 = new InlineValidator(); diff --git a/src/FluentValidation.Tests/TransformTests.cs b/src/FluentValidation.Tests/TransformTests.cs deleted file mode 100644 index 52a56ab62..000000000 --- a/src/FluentValidation.Tests/TransformTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -#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 - -namespace FluentValidation.Tests; - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -#pragma warning disable CS0618 - -public class TransformTests { - [Fact] - public void Transforms_property_value() { - var validator = new InlineValidator(); - validator.Transform(x => x.Surname, name => "foo" + name).Equal("foobar"); - - var result = validator.Validate(new Person {Surname = "bar"}); - result.IsValid.ShouldBeTrue(); - } - - [Fact] - public void Transforms_property_value_to_another_type() { - var validator = new InlineValidator(); - validator.Transform(x => x.Surname, name => 1).GreaterThan(10); - - var result = validator.Validate(new Person {Surname = "bar"}); - result.IsValid.ShouldBeFalse(); - result.Errors[0].ErrorCode.ShouldEqual("GreaterThanValidator"); - } - - [Fact] - public void Transforms_property_value_with_propagated_original_object() { - var validator = new InlineValidator(); - validator.Transform(x => x.Forename, (person, forename) => new {Nicks = person.NickNames, Name = forename}) - .Must(context => context.Nicks.Any(nick => nick == context.Name.ToLower())); - - var result = validator.Validate(new Person {NickNames = new[] {"good11", "peter"}, Forename = "Peter"}); - result.IsValid.ShouldBeTrue(); - } - - [Fact] - public async Task Transforms_property_value_with_propagated_original_object_async() { - var validator = new InlineValidator(); - validator.Transform(x => x.Forename, (person, forename) => new {Nicks = person.NickNames, Name = forename}) - .Must(context => context.Nicks.Any(nick => nick == context.Name.ToLower())); - - var result = await validator.ValidateAsync(new Person {NickNames = new[] {"good11", "peter"}, Forename = "Peter"}); - result.IsValid.ShouldBeTrue(); - } - - [Fact] - public void Transforms_collection_element() { - var validator = new InlineValidator(); - validator.TransformForEach(x => x.Orders, order => order.Amount) - .GreaterThan(0); - - var result = validator.Validate(new Person() {Orders = new List {new Order()}}); - result.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Transforms_collection_element_async() { - var validator = new InlineValidator(); - validator.TransformForEach(x => x.Orders, order => order.Amount) - .MustAsync((amt, token) => Task.FromResult(amt > 0)); - - var result = await validator.ValidateAsync(new Person() {Orders = new List {new Order()}}); - result.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Transforms_collection_element_with_propagated_original_object() { - var validator = new InlineValidator(); - validator.TransformForEach(x => x.Children, (parent, children) => new {ParentName = parent.Surname, Children = children}) - .Must(context => context.ParentName == context.Children.Surname); - - var child = new Person {Surname = "Pupa"}; - var result = validator.Validate(new Person() {Surname = "Lupa", Children = new List {child}}); - result.IsValid.ShouldBeFalse(); - result.Errors.Count.ShouldEqual(1); - } - - [Fact] - public async Task Transforms_collection_element_with_propagated_original_object_async() { - var validator = new InlineValidator(); - validator.TransformForEach(x => x.Children, (parent, children) => new {ParentName = parent.Surname, Children = children}) - .Must(context => context.ParentName == context.Children.Surname); - - var child = new Person {Surname = "Pupa"}; - var result = await validator.ValidateAsync(new Person() {Surname = "Lupa", Children = new List {child}}); - result.IsValid.ShouldBeFalse(); - result.Errors.Count.ShouldEqual(1); - } - - [Fact] - public void Transform_collection_index_builder_and_condition() { - var validator = new InlineValidator(); - validator.TransformForEach(x => x.Orders, to: order => order.Amount) - .Where(amt => amt < 20) - .OverrideIndexer((person, collection, amt, numericIndex) => $"[{numericIndex}_{amt}]") - .LessThan(10); - - var result = validator.Validate(new Person { - Orders = new List { - new Order {Amount = 21}, // Fails condition, skips validation - new Order {Amount = 12}, // Passes condition, fails validation - new Order {Amount = 9}, // Passes condition, passes validation - } - }); - - result.Errors.Count.ShouldEqual(1); - result.Errors[0].PropertyName.ShouldEqual("Orders[1_12]"); - } -} diff --git a/src/FluentValidation.Tests/ValidatorTesterTester.cs b/src/FluentValidation.Tests/ValidatorTesterTester.cs index e077fab2f..ea6c15a2e 100644 --- a/src/FluentValidation.Tests/ValidatorTesterTester.cs +++ b/src/FluentValidation.Tests/ValidatorTesterTester.cs @@ -363,7 +363,7 @@ public class ValidatorTesterTester { } var ex = Assert.Throws(() => - validator.TestValidate(new Person { }).ShouldHaveAnyValidationError().WithoutErrorMessage(withoutErrMsg)); + validator.TestValidate(new Person { }).ShouldHaveValidationErrors().WithoutErrorMessage(withoutErrMsg)); ex.Message.ShouldEqual($"Found an unexpected error message of '{withoutErrMsg}'"); } @@ -641,8 +641,8 @@ public class ValidatorTesterTester { var person = new Person() { Surname = "c" }; var result = validator.TestValidate(person); - result.ShouldHaveAnyValidationError().WithErrorCode("nota"); - result.ShouldHaveAnyValidationError().WithErrorCode("notb"); + result.ShouldHaveValidationErrors().WithErrorCode("nota"); + result.ShouldHaveValidationErrors().WithErrorCode("notb"); } [Fact] @@ -654,11 +654,11 @@ public class ValidatorTesterTester { var resultWithFailure = validator.TestValidate(new Person { Surname = "foo"}); var resultWithoutFailure = validator.TestValidate(new Person { Surname = ""}); - Assert.Throws(() => resultWithoutFailure.ShouldHaveAnyValidationError()); + Assert.Throws(() => resultWithoutFailure.ShouldHaveValidationErrors()); Assert.Throws(() => resultWithFailure.ShouldNotHaveAnyValidationErrors()); // Neither should throw. - resultWithFailure.ShouldHaveAnyValidationError(); + resultWithFailure.ShouldHaveValidationErrors(); resultWithoutFailure.ShouldNotHaveAnyValidationErrors(); } diff --git a/src/FluentValidation/AbstractValidator.cs b/src/FluentValidation/AbstractValidator.cs index d3cc503c3..381cd249f 100644 --- a/src/FluentValidation/AbstractValidator.cs +++ b/src/FluentValidation/AbstractValidator.cs @@ -38,59 +38,6 @@ public abstract class AbstractValidator : IValidator, IEnumerable _classLevelCascadeMode = () => ValidatorOptions.Global.DefaultClassLevelCascadeMode; private Func _ruleLevelCascadeMode = () => ValidatorOptions.Global.DefaultRuleLevelCascadeMode; -#pragma warning disable 618 - /// - /// - /// Gets a single mode value representing the default values of - /// - /// and ., based on the same logic as used when setting - /// this property as described below. - /// - /// - /// Sets the values of - /// and . - /// - /// - /// If set to or , then both properties are set - /// to that value. - /// - /// - /// If set to the deprecated , - /// then - /// is set to , and - /// is set to . - /// This results in the same behaviour as before this property was deprecated. - /// - /// - [Obsolete($"Use {nameof(ClassLevelCascadeMode)} and/or {nameof(RuleLevelCascadeMode)} instead. " + - "CascadeMode will be removed in a future release. " + - "For more details, see https://docs.fluentvalidation.net/en/latest/cascade.html")] - public CascadeMode CascadeMode { - get { - if (ClassLevelCascadeMode == RuleLevelCascadeMode) { - return ClassLevelCascadeMode; - } - else if (ClassLevelCascadeMode == CascadeMode.Continue && RuleLevelCascadeMode == CascadeMode.Stop) { - return CascadeMode.StopOnFirstFailure; - } - else { - throw new Exception( - $"There is no conversion to a single {nameof(CascadeMode)} value from the current combination of " + - $"{nameof(ClassLevelCascadeMode)} and {nameof(RuleLevelCascadeMode)}. " + - $"Please use these properties instead of the deprecated {nameof(CascadeMode)} going forward."); - } - } - set { - ClassLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Continue - : value; - - RuleLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Stop - : value; - } - } - /// /// /// Sets the cascade behaviour in between rules in this validator. @@ -106,17 +53,10 @@ public abstract class AbstractValidator : IValidator, IEnumerablewithin individual rules is controlled by /// . /// - /// - /// This cannot be set to the deprecated . - /// . Attempting to do so it will actually - /// result in being used. - /// /// public CascadeMode ClassLevelCascadeMode { get => _classLevelCascadeMode(); - set => _classLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? () => CascadeMode.Stop - : () => value; + set => _classLevelCascadeMode = () => value; } /// @@ -134,27 +74,19 @@ public abstract class AbstractValidator : IValidator, IEnumerable /// Note that cascade behaviour between rules is controlled by . /// - /// - /// This cannot be set to the deprecated . - /// . Attempting to do so it will actually - /// result in being used. - /// /// public CascadeMode RuleLevelCascadeMode { get => _ruleLevelCascadeMode(); - set => _ruleLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? () => CascadeMode.Stop - : () => value; + set => _ruleLevelCascadeMode = () => value; } -#pragma warning restore 618 ValidationResult IValidator.Validate(IValidationContext context) { - context.Guard("Cannot pass null to Validate", nameof(context)); + ArgumentNullException.ThrowIfNull(context); return Validate(ValidationContext.GetFromNonGenericContext(context)); } Task IValidator.ValidateAsync(IValidationContext context, CancellationToken cancellation) { - context.Guard("Cannot pass null to Validate", nameof(context)); + ArgumentNullException.ThrowIfNull(context); return ValidateAsync(ValidationContext.GetFromNonGenericContext(context), cancellation); } @@ -181,7 +113,7 @@ public Task ValidateAsync(T instance, CancellationToken cancel /// Validation Context /// A ValidationResult object containing any validation failures. public virtual ValidationResult Validate(ValidationContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentNullException.ThrowIfNull(context); // Note: Sync-over-async is OK in this scenario. // The use of the `useAsync` parameter ensures that no async code is @@ -214,8 +146,8 @@ ValueTask completedValueTask /// Validation Context /// Cancellation token /// A ValidationResult object containing any validation failures. - public virtual async Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()) { - if (context == null) throw new ArgumentNullException(nameof(context)); + public virtual async Task ValidateAsync(ValidationContext context, CancellationToken cancellation = default) { + ArgumentNullException.ThrowIfNull(context); context.IsAsync = true; return await ValidateInternalAsync(context, useAsync: true, cancellation); } @@ -232,9 +164,9 @@ ValueTask completedValueTask return result; } -#pragma warning disable CS0618 - EnsureInstanceNotNull(context.InstanceToValidate); -#pragma warning restore CS0618 + if (context.InstanceToValidate == null) { + throw new InvalidOperationException("Cannot pass a null model to Validate/ValidateAsync. The root model must be non-null."); + } int count = Rules.Count; @@ -276,7 +208,7 @@ ValueTask completedValueTask public virtual IValidatorDescriptor CreateDescriptor() => new ValidatorDescriptor(Rules); bool IValidator.CanValidateInstancesOfType(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + ArgumentNullException.ThrowIfNull(type); return typeof(T).IsAssignableFrom(type); } @@ -290,54 +222,13 @@ ValueTask completedValueTask /// The expression representing the property to validate /// an IRuleBuilder instance on which validators can be defined public IRuleBuilderInitial RuleFor(Expression> expression) { - expression.Guard("Cannot pass null to RuleFor", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var rule = PropertyRule.Create(expression, () => RuleLevelCascadeMode); Rules.Add(rule); OnRuleAdded(rule); return new RuleBuilder(rule, this); } - /// - /// Defines a validation rule for a specify property and transform it to a different type. - /// - /// - /// Transform(x => x.OrderNumber, to: orderNumber => orderNumber.ToString())... - /// - /// The type of property being validated - /// The type after the transformer has been applied - /// The expression representing the property to transform - /// Function to transform the property value into a different type - /// an IRuleBuilder instance on which validators can be defined - [Obsolete("The Transform method is deprecated and will be removed in FluentValidation 12. We recommend using a computed property on your model instead. For details see https://github.com/FluentValidation/FluentValidation/issues/2072")] - public IRuleBuilderInitial Transform(Expression> from, Func to) { - from.Guard("Cannot pass null to Transform", nameof(from)); - var rule = PropertyRule.Create(from, to, () => RuleLevelCascadeMode); - Rules.Add(rule); - OnRuleAdded(rule); - return new RuleBuilder(rule, this); - } - - /// - /// Defines a validation rule for a specify property and transform it to a different type. - /// - /// - /// Transform(x => x.OrderNumber, to: orderNumber => orderNumber.ToString())... - /// - /// The type of property being validated - /// The type after the transformer has been applied - /// The expression representing the property to transform - /// Function to transform the property value into a different type - /// an IRuleBuilder instance on which validators can be defined - [Obsolete("The Transform method is deprecated and will be removed in FluentValidation 12. We recommend using a computed property on your model instead. For details see https://github.com/FluentValidation/FluentValidation/issues/2072")] - public IRuleBuilderInitial Transform(Expression> from, Func to) { - from.Guard("Cannot pass null to Transform", nameof(from)); - var rule = PropertyRule.Create(from, to, () => RuleLevelCascadeMode); - Rules.Add(rule); - OnRuleAdded(rule); - return new RuleBuilder(rule, this); - } - - /// /// Invokes a rule for each item in the collection. /// @@ -345,55 +236,21 @@ ValueTask completedValueTask /// Expression representing the collection to validate /// An IRuleBuilder instance on which validators can be defined public IRuleBuilderInitialCollection RuleForEach(Expression>> expression) { - expression.Guard("Cannot pass null to RuleForEach", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var rule = CollectionPropertyRule.Create(expression, () => RuleLevelCascadeMode); Rules.Add(rule); OnRuleAdded(rule); return new RuleBuilder(rule, this); } - /// - /// Invokes a rule for each item in the collection, transforming the element from one type to another. - /// - /// Type of property - /// The type after the transformer has been applied - /// Expression representing the collection to validate - /// Function to transform the collection element into a different type - /// An IRuleBuilder instance on which validators can be defined - [Obsolete("The TransformForEach method is deprecated and will be removed in FluentValidation 12. We recommend using a computed property on your model instead. For details see https://github.com/FluentValidation/FluentValidation/issues/2072")] - public IRuleBuilderInitialCollection TransformForEach(Expression>> expression, Func to) { - expression.Guard("Cannot pass null to RuleForEach", nameof(expression)); - var rule = CollectionPropertyRule.CreateTransformed(expression, to, () => RuleLevelCascadeMode); - Rules.Add(rule); - OnRuleAdded(rule); - return new RuleBuilder(rule, this); - } - - /// - /// Invokes a rule for each item in the collection, transforming the element from one type to another. - /// - /// Type of property - /// The type after the transformer has been applied - /// Expression representing the collection to validate - /// Function to transform the collection element into a different type - /// An IRuleBuilder instance on which validators can be defined - [Obsolete("The TransformForEach method is deprecated and will be removed in FluentValidation 12. We recommend using a computed property on your model instead. For details see https://github.com/FluentValidation/FluentValidation/issues/2072")] - public IRuleBuilderInitialCollection TransformForEach(Expression>> expression, Func to) { - expression.Guard("Cannot pass null to RuleForEach", nameof(expression)); - var rule = CollectionPropertyRule.CreateTransformed(expression, to, () => RuleLevelCascadeMode); - Rules.Add(rule); - OnRuleAdded(rule); - return new RuleBuilder(rule, this); - } - /// /// Defines a RuleSet that can be used to group together several validators. /// /// The name of the ruleset. /// Action that encapsulates the rules in the ruleset. public void RuleSet(string ruleSetName, Action action) { - ruleSetName.Guard("A name must be specified when calling RuleSet.", nameof(ruleSetName)); - action.Guard("A ruleset definition must be specified when calling RuleSet.", nameof(action)); + ExtensionsInternal.ThrowIfNullOrEmpty(ruleSetName); + ArgumentNullException.ThrowIfNull(action); var ruleSetNames = ruleSetName.Split(',', ';') .Select(x => x.Trim()) @@ -410,8 +267,11 @@ ValueTask completedValueTask /// The condition that should apply to multiple rules /// Action that encapsulates the rules. /// - public IConditionBuilder When(Func predicate, Action action) - => When((x, _) => predicate(x), action); + public IConditionBuilder When(Func predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return When((x, _) => predicate(x), action); + } /// /// Defines a condition that applies to several rules @@ -419,24 +279,33 @@ public IConditionBuilder When(Func predicate, Action action) /// The condition that should apply to multiple rules /// Action that encapsulates the rules. /// - public IConditionBuilder When(Func, bool> predicate, Action action) - => new ConditionBuilder(Rules).When(predicate, action); + public IConditionBuilder When(Func, bool> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return new ConditionBuilder(Rules).When(predicate, action); + } /// /// Defines an inverse condition that applies to several rules /// /// The condition that should be applied to multiple rules /// Action that encapsulates the rules - public IConditionBuilder Unless(Func predicate, Action action) - => Unless((x, _) => predicate(x), action); + public IConditionBuilder Unless(Func predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return Unless((x, _) => predicate(x), action); + } /// /// Defines an inverse condition that applies to several rules /// /// The condition that should be applied to multiple rules /// Action that encapsulates the rules - public IConditionBuilder Unless(Func, bool> predicate, Action action) - => new ConditionBuilder(Rules).Unless(predicate, action); + public IConditionBuilder Unless(Func, bool> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return new ConditionBuilder(Rules).Unless(predicate, action); + } /// /// Defines an asynchronous condition that applies to several rules @@ -444,8 +313,11 @@ public IConditionBuilder Unless(Func, bool> predicate, A /// The asynchronous condition that should apply to multiple rules /// Action that encapsulates the rules. /// - public IConditionBuilder WhenAsync(Func> predicate, Action action) - => WhenAsync((x, _, cancel) => predicate(x, cancel), action); + public IConditionBuilder WhenAsync(Func> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return WhenAsync((x, _, cancel) => predicate(x, cancel), action); + } /// /// Defines an asynchronous condition that applies to several rules @@ -453,30 +325,39 @@ public IConditionBuilder WhenAsync(Func> predic /// The asynchronous condition that should apply to multiple rules /// Action that encapsulates the rules. /// - public IConditionBuilder WhenAsync(Func, CancellationToken, Task> predicate, Action action) - => new AsyncConditionBuilder(Rules).WhenAsync(predicate, action); + public IConditionBuilder WhenAsync(Func, CancellationToken, Task> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return new AsyncConditionBuilder(Rules).WhenAsync(predicate, action); + } /// /// Defines an inverse asynchronous condition that applies to several rules /// /// The asynchronous condition that should be applied to multiple rules /// Action that encapsulates the rules - public IConditionBuilder UnlessAsync(Func> predicate, Action action) - => UnlessAsync((x, _, cancel) => predicate(x, cancel), action); + public IConditionBuilder UnlessAsync(Func> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return UnlessAsync((x, _, cancel) => predicate(x, cancel), action); + } /// /// Defines an inverse asynchronous condition that applies to several rules /// /// The asynchronous condition that should be applied to multiple rules /// Action that encapsulates the rules - public IConditionBuilder UnlessAsync(Func, CancellationToken, Task> predicate, Action action) - => new AsyncConditionBuilder(Rules).UnlessAsync(predicate, action); + public IConditionBuilder UnlessAsync(Func, CancellationToken, Task> predicate, Action action) { + ArgumentNullException.ThrowIfNull(predicate); + ArgumentNullException.ThrowIfNull(action); + return new AsyncConditionBuilder(Rules).UnlessAsync(predicate, action); + } /// /// Includes the rules from the specified validator /// public void Include(IValidator rulesToInclude) { - rulesToInclude.Guard("Cannot pass null to Include", nameof(rulesToInclude)); + ArgumentNullException.ThrowIfNull(rulesToInclude); var rule = IncludeRule.Create(rulesToInclude, () => RuleLevelCascadeMode); Rules.Add(rule); OnRuleAdded(rule); @@ -486,7 +367,7 @@ public IConditionBuilder UnlessAsync(Func, CancellationT /// Includes the rules from the specified validator /// public void Include(Func rulesToInclude) where TValidator : IValidator { - rulesToInclude.Guard("Cannot pass null to Include", nameof(rulesToInclude)); + ArgumentNullException.ThrowIfNull(rulesToInclude); var rule = IncludeRule.Create(rulesToInclude, () => RuleLevelCascadeMode); Rules.Add(rule); OnRuleAdded(rule); @@ -504,15 +385,7 @@ public IConditionBuilder UnlessAsync(Func, CancellationT IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// - /// Throws an exception if the instance being validated is null. - /// - /// - [Obsolete("Overriding the EnsureInstanceNotNull method to prevent FluentValidation for throwing an exception for null root models is no longer supported or recommended. The ability to override this method will be removed in FluentValidation 12. For details, see https://github.com/FluentValidation/FluentValidation/issues/2069")] - protected virtual void EnsureInstanceNotNull(object instanceToValidate) - => instanceToValidate.Guard("Cannot pass null model to Validate.", nameof(instanceToValidate)); - - /// - /// Determines if validation should occur and provides a means to modify the context and ValidationResult prior to execution. + /// Determines if validation should occtur and provides a means to modify the context and ValidationResult prior to execution. /// If this method returns false, then the ValidationResult is immediately returned from Validate/ValidateAsync. /// /// diff --git a/src/FluentValidation/DefaultValidatorExtensions.cs b/src/FluentValidation/DefaultValidatorExtensions.cs index 7ad162d2f..976a55ff2 100644 --- a/src/FluentValidation/DefaultValidatorExtensions.cs +++ b/src/FluentValidation/DefaultValidatorExtensions.cs @@ -417,7 +417,7 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions Must(this IRuleBuilder ruleBuilder, Func predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return ruleBuilder.Must((x, val) => predicate(val)); } @@ -433,7 +433,7 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions Must(this IRuleBuilder ruleBuilder, Func predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return ruleBuilder.Must((x, val, _) => predicate(x, val)); } @@ -449,8 +449,8 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions Must(this IRuleBuilder ruleBuilder, Func, bool> predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); - return ruleBuilder.SetValidator(new PredicateValidator((instance, property, propertyValidatorContext) => predicate(instance, property, propertyValidatorContext))); + ArgumentNullException.ThrowIfNull(predicate); + return ruleBuilder.SetValidator(new PredicateValidator(predicate)); } /// @@ -464,7 +464,7 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions MustAsync(this IRuleBuilder ruleBuilder, Func> predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return ruleBuilder.MustAsync((x, val, ctx, cancel) => predicate(val, cancel)); } @@ -481,7 +481,7 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions MustAsync(this IRuleBuilder ruleBuilder, Func> predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return ruleBuilder.MustAsync((x, val, _, cancel) => predicate(x, val, cancel)); } @@ -497,7 +497,7 @@ public static partial class DefaultValidatorExtensions { /// A lambda expression specifying the predicate /// public static IRuleBuilderOptions MustAsync(this IRuleBuilder ruleBuilder, Func, CancellationToken, Task> predicate) { - predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return ruleBuilder.SetAsyncValidator(new AsyncPredicateValidator(predicate)); } @@ -635,7 +635,7 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : IComparable, IComparable { - expression.Guard("Cannot pass null to LessThan", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); @@ -656,7 +656,7 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - expression.Guard("Cannot pass null to LessThan", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); @@ -681,7 +681,7 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - expression.Guard("Cannot pass null to LessThan", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); @@ -703,7 +703,7 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - expression.Guard("Cannot pass null to LessThan", nameof(expression)); + ArgumentNullException.ThrowIfNull(expression); var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); @@ -728,6 +728,7 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -748,6 +749,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -771,6 +774,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -791,6 +796,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -814,6 +821,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -834,6 +843,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -857,6 +868,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -877,6 +890,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(expression); + var member = expression.GetMember(); var func = AccessorCache.GetCachedAccessor(member, expression); var name = GetDisplayName(member, expression); @@ -900,6 +915,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : IComparable, IComparable { + ArgumentNullException.ThrowIfNull(valueToCompare); + var member = valueToCompare.GetMember(); var func = AccessorCache.GetCachedAccessor(member, valueToCompare); var name = GetDisplayName(member, valueToCompare); @@ -920,6 +937,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(valueToCompare); + var member = valueToCompare.GetMember(); var func = AccessorCache.GetCachedAccessor(member, valueToCompare); var name = GetDisplayName(member, valueToCompare); @@ -942,6 +961,8 @@ public static partial class DefaultValidatorExtensions { /// public static IRuleBuilderOptions GreaterThanOrEqualTo(this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(valueToCompare); + var member = valueToCompare.GetMember(); var func = AccessorCache.GetCachedAccessor(member, valueToCompare); var name = GetDisplayName(member, valueToCompare); @@ -965,6 +986,8 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : struct, IComparable, IComparable { + ArgumentNullException.ThrowIfNull(valueToCompare); + var member = valueToCompare.GetMember(); var func = AccessorCache.GetCachedAccessor(member, valueToCompare); var name = GetDisplayName(member, valueToCompare); @@ -1070,32 +1093,6 @@ public static partial class DefaultValidatorExtensions { public static IRuleBuilderOptions IsInEnum(this IRuleBuilder ruleBuilder) => ruleBuilder.SetValidator(new EnumValidator()); - /// - /// Defines a scale precision validator on the current rule builder that ensures that the specific value has a certain scale and precision - /// - /// Type of object being validated - /// The rule builder on which the validator should be defined - /// Allowed scale of the value - /// Allowed precision of the value - /// Whether the validator will ignore trailing zeros. - /// - [Obsolete("Please use the PrecisionScale method instead, which takes precision as the first parameter and scale as the second. See https://github.com/FluentValidation/FluentValidation/issues/2030 for further details")] - public static IRuleBuilderOptions ScalePrecision(this IRuleBuilder ruleBuilder, int scale, int precision, bool ignoreTrailingZeros = false) - => ruleBuilder.SetValidator(new ScalePrecisionValidator(scale, precision) { IgnoreTrailingZeros = ignoreTrailingZeros }); - - /// - /// Defines a scale precision validator on the current rule builder that ensures that the specific value has a certain scale and precision - /// - /// Type of object being validated - /// The rule builder on which the validator should be defined - /// Allowed scale of the value - /// Allowed precision of the value - /// Whether the validator will ignore trailing zeros. - /// - [Obsolete("Please use the PrecisionScale method instead, which takes precision as the first parameter and scale as the second. See https://github.com/FluentValidation/FluentValidation/issues/2030 for further details")] - public static IRuleBuilderOptions ScalePrecision(this IRuleBuilder ruleBuilder, int scale, int precision, bool ignoreTrailingZeros = false) - => ruleBuilder.SetValidator(new ScalePrecisionValidator(scale, precision) { IgnoreTrailingZeros = ignoreTrailingZeros }); - /// /// Defines a scale precision validator on the current rule builder that ensures a decimal the specified precision and scale. /// @@ -1106,7 +1103,7 @@ public static partial class DefaultValidatorExtensions { /// Whether the validator will ignore trailing zeros after the decimal point. For example, when set to true the decimal 123.4500 will be considered to have a precision of 5 and scale of 2. When set to false, it will be considered to have a precision of 7 and scale of 4. /// public static IRuleBuilderOptions PrecisionScale(this IRuleBuilder ruleBuilder, int precision, int scale, bool ignoreTrailingZeros) - => ruleBuilder.SetValidator(new ScalePrecisionValidator(scale, precision) { IgnoreTrailingZeros = ignoreTrailingZeros }); + => ruleBuilder.SetValidator(new PrecisionScaleValidator(precision, scale, ignoreTrailingZeros)); /// /// Defines a scale precision validator on the current rule builder that ensures a decimal the specified precision and scale. @@ -1118,7 +1115,7 @@ public static partial class DefaultValidatorExtensions { /// Whether the validator will ignore trailing zeros after the decimal point. For example, when set to true the decimal 123.4500 will be considered to have a precision of 5 and scale of 2. When set to false, it will be considered to have a precision of 7 and scale of 4. /// public static IRuleBuilderOptions PrecisionScale(this IRuleBuilder ruleBuilder, int precision, int scale, bool ignoreTrailingZeros) - => ruleBuilder.SetValidator(new ScalePrecisionValidator(scale, precision) { IgnoreTrailingZeros = ignoreTrailingZeros }); + => ruleBuilder.SetValidator(new PrecisionScaleValidator(precision, scale, ignoreTrailingZeros)); /// /// Defines a custom validation rule diff --git a/src/FluentValidation/DefaultValidatorOptions.cs b/src/FluentValidation/DefaultValidatorOptions.cs index 118b3a20e..a3086ee9e 100644 --- a/src/FluentValidation/DefaultValidatorOptions.cs +++ b/src/FluentValidation/DefaultValidatorOptions.cs @@ -88,9 +88,6 @@ public static class DefaultValidatorOptions { /// /// If set to then all validators in the chain will execute regardless of failures. /// - /// - /// If set to the deprecated , behavior is as with . - /// /// public static IRuleBuilderInitial Cascade(this IRuleBuilderInitial ruleBuilder, CascadeMode cascadeMode) { Configurable(ruleBuilder).CascadeMode = cascadeMode; @@ -107,9 +104,6 @@ public static class DefaultValidatorOptions { /// /// If set to then all validators in the chain will execute regardless of failures. /// - /// - /// If set to the deprecated , behaviour is as with . - /// /// public static IRuleBuilderInitialCollection Cascade(this IRuleBuilderInitialCollection ruleBuilder, CascadeMode cascadeMode) { Configurable(ruleBuilder).CascadeMode = cascadeMode; @@ -123,7 +117,7 @@ public static class DefaultValidatorOptions { /// The error message to use /// public static IRuleBuilderOptions WithMessage(this IRuleBuilderOptions rule, string errorMessage) { - errorMessage.Guard("A message must be specified when calling WithMessage.", nameof(errorMessage)); + ExtensionsInternal.ThrowIfNullOrEmpty(errorMessage); Configurable(rule).Current.SetErrorMessage(errorMessage); return rule; } @@ -135,7 +129,7 @@ public static class DefaultValidatorOptions { /// Delegate that will be invoked to retrieve the localized message. /// public static IRuleBuilderOptions WithMessage(this IRuleBuilderOptions rule, Func messageProvider) { - messageProvider.Guard("A messageProvider must be provided.", nameof(messageProvider)); + ArgumentNullException.ThrowIfNull(messageProvider); Configurable(rule).Current.SetErrorMessage((ctx, val) => { return messageProvider(ctx == null ? default : ctx.InstanceToValidate); }); @@ -149,7 +143,7 @@ public static class DefaultValidatorOptions { /// Delegate that will be invoked.Uses_localized_name to retrieve the localized message. /// public static IRuleBuilderOptions WithMessage(this IRuleBuilderOptions rule, Func messageProvider) { - messageProvider.Guard("A messageProvider must be provided.", nameof(messageProvider)); + ArgumentNullException.ThrowIfNull(messageProvider); Configurable(rule).Current.SetErrorMessage((context, value) => { return messageProvider(context == null ? default : context.InstanceToValidate, value); }); @@ -163,7 +157,7 @@ public static class DefaultValidatorOptions { /// The error code to use /// public static IRuleBuilderOptions WithErrorCode(this IRuleBuilderOptions rule, string errorCode) { - errorCode.Guard("A error code must be specified when calling WithErrorCode.", nameof(errorCode)); + ExtensionsInternal.ThrowIfNullOrEmpty(errorCode); Configurable(rule).Current.ErrorCode = errorCode; return rule; } @@ -177,7 +171,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions When(this IRuleBuilderOptions rule, Func predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling When.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.When((x, ctx) => predicate(x), applyConditionTo); } @@ -190,7 +184,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions When(this IRuleBuilderOptionsConditions rule, Func predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling When.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.When((x, ctx) => predicate(x), applyConditionTo); } @@ -203,7 +197,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions When(this IRuleBuilderOptions rule, Func, bool> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling When.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); // Default behaviour for When/Unless as of v1.3 is to apply the condition to all previous validators in the chain. Configurable(rule).ApplyCondition(ctx => predicate((T)ctx.InstanceToValidate, ValidationContext.GetFromNonGenericContext(ctx)), applyConditionTo); return rule; @@ -218,7 +212,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions When(this IRuleBuilderOptionsConditions rule, Func, bool> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling When.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); // Default behaviour for When/Unless as of v1.3 is to apply the condition to all previous validators in the chain. Configurable(rule).ApplyCondition(ctx => predicate((T)ctx.InstanceToValidate, ValidationContext.GetFromNonGenericContext(ctx)), applyConditionTo); return rule; @@ -233,7 +227,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions Unless(this IRuleBuilderOptions rule, Func predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling Unless", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.Unless((x, ctx) => predicate(x), applyConditionTo); } @@ -246,7 +240,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions Unless(this IRuleBuilderOptionsConditions rule, Func predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling Unless", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.Unless((x, ctx) => predicate(x), applyConditionTo); } @@ -259,7 +253,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions Unless(this IRuleBuilderOptions rule, Func, bool> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling Unless", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.When((x, ctx) => !predicate(x, ctx), applyConditionTo); } @@ -272,7 +266,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions Unless(this IRuleBuilderOptionsConditions rule, Func, bool> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling Unless", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.When((x, ctx) => !predicate(x, ctx), applyConditionTo); } @@ -285,7 +279,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions WhenAsync(this IRuleBuilderOptions rule, Func> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling WhenAsync.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.WhenAsync((x, ctx, ct) => predicate(x, ct), applyConditionTo); } @@ -298,7 +292,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions WhenAsync(this IRuleBuilderOptionsConditions rule, Func> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling WhenAsync.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.WhenAsync((x, ctx, ct) => predicate(x, ct), applyConditionTo); } @@ -311,7 +305,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions WhenAsync(this IRuleBuilderOptions rule, Func, CancellationToken, Task> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling WhenAsync.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); // Default behaviour for When/Unless as of v1.3 is to apply the condition to all previous validators in the chain. Configurable(rule).ApplyAsyncCondition((ctx, ct) => predicate((T)ctx.InstanceToValidate, ValidationContext.GetFromNonGenericContext(ctx), ct), applyConditionTo); return rule; @@ -326,7 +320,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions WhenAsync(this IRuleBuilderOptionsConditions rule, Func, CancellationToken, Task> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling WhenAsync.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); // Default behaviour for When/Unless as of v1.3 is to apply the condition to all previous validators in the chain. Configurable(rule).ApplyAsyncCondition((ctx, ct) => predicate((T)ctx.InstanceToValidate, ValidationContext.GetFromNonGenericContext(ctx), ct), applyConditionTo); return rule; @@ -341,7 +335,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions UnlessAsync(this IRuleBuilderOptions rule, Func> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling UnlessAsync", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.UnlessAsync((x, ctx, ct) => predicate(x, ct), applyConditionTo); } @@ -354,7 +348,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions UnlessAsync(this IRuleBuilderOptionsConditions rule, Func> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling UnlessAsync", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.UnlessAsync((x, ctx, ct) => predicate(x, ct), applyConditionTo); } @@ -367,7 +361,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptions UnlessAsync(this IRuleBuilderOptions rule, Func, CancellationToken, Task> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling UnlessAsync", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.WhenAsync(async (x, ctx, ct) => !await predicate(x, ctx, ct), applyConditionTo); } @@ -380,7 +374,7 @@ public static class DefaultValidatorOptions { /// Whether the condition should be applied to the current rule or all rules in the chain /// public static IRuleBuilderOptionsConditions UnlessAsync(this IRuleBuilderOptionsConditions rule, Func, CancellationToken, Task> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators) { - predicate.Guard("A predicate must be specified when calling UnlessAsync", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); return rule.WhenAsync(async (x, ctx, ct) => !await predicate(x, ctx, ct), applyConditionTo); } @@ -392,7 +386,7 @@ public static class DefaultValidatorOptions { /// public static IRuleBuilderInitialCollection Where(this IRuleBuilderInitialCollection rule, Func predicate) { // This overload supports RuleFor().SetCollectionValidator() (which returns IRuleBuilderOptions>) - predicate.Guard("Cannot pass null to Where.", nameof(predicate)); + ArgumentNullException.ThrowIfNull(predicate); Configurable(rule).Filter = predicate; return rule; } @@ -404,7 +398,7 @@ public static class DefaultValidatorOptions { /// The property name to use /// public static IRuleBuilderOptions WithName(this IRuleBuilderOptions rule, string overridePropertyName) { - overridePropertyName.Guard("A property name must be specified when calling WithName.", nameof(overridePropertyName)); + ExtensionsInternal.ThrowIfNullOrEmpty(overridePropertyName); Configurable(rule).SetDisplayName(overridePropertyName); return rule; } @@ -416,7 +410,7 @@ public static class DefaultValidatorOptions { /// Func used to retrieve the property's display name /// public static IRuleBuilderOptions WithName(this IRuleBuilderOptions rule, Func nameProvider) { - nameProvider.Guard("A nameProvider WithName.", nameof(nameProvider)); + ArgumentNullException.ThrowIfNull(nameProvider); // Must use null propagation here. // The MVC clientside validation will try and retrieve the name, but won't // be able to to so if we've used this overload of WithName. @@ -464,7 +458,7 @@ public static class DefaultValidatorOptions { /// /// public static IRuleBuilderOptions WithState(this IRuleBuilderOptions rule, Func stateProvider) { - stateProvider.Guard("A lambda expression must be passed to WithState", nameof(stateProvider)); + ArgumentNullException.ThrowIfNull(stateProvider); var wrapper = new Func, TProperty, object>((ctx, _) => stateProvider(ctx.InstanceToValidate)); Configurable(rule).Current.CustomStateProvider = wrapper; return rule; @@ -479,7 +473,7 @@ public static class DefaultValidatorOptions { /// /// public static IRuleBuilderOptions WithState(this IRuleBuilderOptions rule, Func stateProvider) { - stateProvider.Guard("A lambda expression must be passed to WithState", nameof(stateProvider)); + ArgumentNullException.ThrowIfNull(stateProvider); var wrapper = new Func, TProperty, object>((ctx, val) => { return stateProvider(ctx.InstanceToValidate, val); @@ -511,7 +505,7 @@ public static class DefaultValidatorOptions { /// /// public static IRuleBuilderOptions WithSeverity(this IRuleBuilderOptions rule, Func severityProvider) { - severityProvider.Guard("A lambda expression must be passed to WithSeverity", nameof(severityProvider)); + ArgumentNullException.ThrowIfNull(severityProvider); Severity SeverityProvider(ValidationContext ctx, TProperty value) { return severityProvider(ctx.InstanceToValidate); @@ -530,7 +524,7 @@ public static class DefaultValidatorOptions { /// /// public static IRuleBuilderOptions WithSeverity(this IRuleBuilderOptions rule, Func severityProvider) { - severityProvider.Guard("A lambda expression must be passed to WithSeverity", nameof(severityProvider)); + ArgumentNullException.ThrowIfNull(severityProvider); Severity SeverityProvider(ValidationContext ctx, TProperty value) { return severityProvider(ctx.InstanceToValidate, value); @@ -549,7 +543,7 @@ public static class DefaultValidatorOptions { /// /// public static IRuleBuilderOptions WithSeverity(this IRuleBuilderOptions rule, Func, Severity> severityProvider) { - severityProvider.Guard("A lambda expression must be passed to WithSeverity", nameof(severityProvider)); + ArgumentNullException.ThrowIfNull(severityProvider); Severity SeverityProvider(ValidationContext ctx, TProperty value) { return severityProvider(ctx.InstanceToValidate, value, ctx); @@ -567,7 +561,7 @@ public static class DefaultValidatorOptions { /// public static IRuleBuilderInitialCollection OverrideIndexer(this IRuleBuilderInitialCollection rule, Func, TCollectionElement, int, string> callback) { // This overload supports RuleFor().SetCollectionValidator() (which returns IRuleBuilderOptions>) - callback.Guard("Cannot pass null to OverrideIndexer.", nameof(callback)); + ArgumentNullException.ThrowIfNull(callback); Configurable(rule).IndexBuilder = callback; return rule; } diff --git a/src/FluentValidation/Enums.cs b/src/FluentValidation/Enums.cs index 2bb3a5123..766392430 100644 --- a/src/FluentValidation/Enums.cs +++ b/src/FluentValidation/Enums.cs @@ -28,26 +28,13 @@ public enum CascadeMode { /// When a rule/validator fails, execution continues to the next rule/validator. /// For more information, see the methods/properties that accept this enum as a parameter. /// - Continue, - -#pragma warning disable 618 - /// - /// For more information, see the methods/properties that accept this enum as a parameter. - /// - [Obsolete( - $"The behaviour of {nameof(StopOnFirstFailure)} has been replaced by use of the " + - $"separate validator-level properties {nameof(AbstractValidator.ClassLevelCascadeMode)} " + - $"and {nameof(AbstractValidator.RuleLevelCascadeMode)}, " + - $"and their global default equivalents. {nameof(StopOnFirstFailure)} will be removed in a later release. " + - "For more details, see https://docs.fluentvalidation.net/en/latest/cascade.html .")] - StopOnFirstFailure, -#pragma warning restore 618 + Continue = 0, /// /// When a rule/validator fails, validation is stopped for the current rule/validator. /// For more information, see the methods/properties that accept this enum as a parameter. /// - Stop, + Stop = 2, // Note Stop is 2 for backwards compatibility. The StopOnFirstFailure option was 1, which was removed in 12.0. This is explicitly set to 2 to prevent it from being automatically renumbered to 1. } /// diff --git a/src/FluentValidation/FluentValidation.csproj b/src/FluentValidation/FluentValidation.csproj index 994d405f4..71f70f0ba 100644 --- a/src/FluentValidation/FluentValidation.csproj +++ b/src/FluentValidation/FluentValidation.csproj @@ -1,9 +1,9 @@  - netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 A validation library for .NET that uses a fluent interface to construct strongly-typed validation rules. -FluentValidation 11 is a major release. Please read the upgrade guide at https://docs.fluentvalidation.net/en/latest/upgrading-to-11.html +FluentValidation 12 is a major release. Please read the upgrade guide at https://docs.fluentvalidation.net/en/latest/upgrading-to-12.html Full release notes can be found at https://github.com/FluentValidation/FluentValidation/releases @@ -16,6 +16,7 @@ Full release notes can be found at https://github.com/FluentValidation/FluentVal true true true + false 1591 diff --git a/src/FluentValidation/IValidationContext.cs b/src/FluentValidation/IValidationContext.cs index 82539e2c9..085fb3648 100644 --- a/src/FluentValidation/IValidationContext.cs +++ b/src/FluentValidation/IValidationContext.cs @@ -289,7 +289,7 @@ public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IVal /// The property name /// The error message public void AddFailure(string propertyName, string errorMessage) { - errorMessage.Guard("An error message must be specified when calling AddFailure.", nameof(errorMessage)); + ExtensionsInternal.ThrowIfNullOrEmpty(errorMessage); errorMessage = MessageFormatter.BuildMessage(errorMessage); AddFailure(new ValidationFailure(PropertyChain.BuildPropertyPath(propertyName ?? string.Empty), errorMessage)); } @@ -300,7 +300,7 @@ public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IVal /// /// The error message public void AddFailure(string errorMessage) { - errorMessage.Guard("An error message must be specified when calling AddFailure.", nameof(errorMessage)); + ExtensionsInternal.ThrowIfNullOrEmpty(errorMessage); errorMessage = MessageFormatter.BuildMessage(errorMessage); AddFailure(new ValidationFailure(PropertyPath, errorMessage)); } diff --git a/src/FluentValidation/IValidationRule.cs b/src/FluentValidation/IValidationRule.cs index 19b39fd45..bf8a3cc6e 100644 --- a/src/FluentValidation/IValidationRule.cs +++ b/src/FluentValidation/IValidationRule.cs @@ -33,12 +33,6 @@ public interface IValidationRule : IValidationRule { /// public CascadeMode CascadeMode { get; set; } - /// - /// Sets the display name for the property. - /// - /// The property's display name - void SetDisplayName(string name); - /// /// Sets the display name for the property using a function. /// @@ -106,6 +100,17 @@ public interface IValidationRule : IValidationRule { /// The model from which the property value should be retrieved. /// The property value. object GetPropertyValue(T instance); + + /// + /// Attempts to get the value of a property from the specified instance. + /// + /// The type of the property to retrieve. + /// The instance from which to retrieve the property value. + /// When this method returns, contains the value of the property, if the retrieval was successful; otherwise, the default value for the type of the property. + /// + /// true if the property value was successfully retrieved and is of type ; otherwise, false. + /// + bool TryGetPropertyValue(T instance, out TProp value); } /// @@ -128,6 +133,12 @@ public interface IValidationRule { /// Display name string GetDisplayName(IValidationContext context); + /// + /// Sets the display name for the property. + /// + /// The property's display name + void SetDisplayName(string name); + /// /// Returns the property name for the property being validated. /// Returns null if it is not a property being validated (eg a method call) diff --git a/src/FluentValidation/Internal/AccessorCache.cs b/src/FluentValidation/Internal/AccessorCache.cs index fd7e280e4..126f31f5a 100644 --- a/src/FluentValidation/Internal/AccessorCache.cs +++ b/src/FluentValidation/Internal/AccessorCache.cs @@ -8,19 +8,19 @@ namespace FluentValidation.Internal; /// /// Member accessor cache. /// -/// +/// The type for which to cache member accessors. public static class AccessorCache { private static readonly ConcurrentDictionary _cache = new(); /// - /// Gets an accessor func based on an expression + /// Gets an accessor func based on an expression. /// - /// + /// The type of property. /// The member represented by the expression - /// - /// - /// Cache prefix - /// Accessor func + /// The accessor expression. + /// to bypass the cache, by default. + /// The cache prefix used to distinguish between collection and non-collection access. + /// An accessor func. public static Func GetCachedAccessor(MemberInfo member, Expression> expression, bool bypassCache = false, string cachePrefix = null) { if (bypassCache || ValidatorOptions.Global.DisableAccessorCache) { return expression.Compile(); @@ -43,24 +43,32 @@ public static class AccessorCache { key = new Key(member, expression, cachePrefix); } - return (Func)_cache.GetOrAdd(key, k => expression.Compile()); + return (Func)_cache.GetOrAdd(key, static (_, exp) => exp.Compile(), expression); } public static void Clear() { _cache.Clear(); } + + /// + /// Represents a unique cache key. + /// private class Key { private readonly MemberInfo _memberInfo; - private readonly string _expressionDebugView; + /// + /// The expression key ensures that the accessor is not shared between collection and non-collection access. + /// Collection and non-collection access must use different accessors or a runtime exception will occur. + /// + private readonly string _expressionKey; public Key(MemberInfo member, Expression expression, string cachePrefix) { _memberInfo = member; - _expressionDebugView = cachePrefix != null ? cachePrefix + expression.ToString() : expression.ToString(); + _expressionKey = cachePrefix != null ? cachePrefix + expression.ToString() : expression.ToString(); } - protected bool Equals(Key other) { - return Equals(_memberInfo, other._memberInfo) && string.Equals(_expressionDebugView, other._expressionDebugView); + public bool Equals(Key other) { + return Equals(_memberInfo, other._memberInfo) && string.Equals(_expressionKey, other._expressionKey); } public override bool Equals(object obj) { @@ -72,7 +80,7 @@ private class Key { public override int GetHashCode() { unchecked { - return ((_memberInfo != null ? _memberInfo.GetHashCode() : 0)*397) ^ (_expressionDebugView != null ? _expressionDebugView.GetHashCode() : 0); + return ((_memberInfo != null ? _memberInfo.GetHashCode() : 0)*397) ^ (_expressionKey != null ? _expressionKey.GetHashCode() : 0); } } } diff --git a/src/FluentValidation/Internal/CollectionPropertyRule.cs b/src/FluentValidation/Internal/CollectionPropertyRule.cs index b18316bbb..021e099ac 100644 --- a/src/FluentValidation/Internal/CollectionPropertyRule.cs +++ b/src/FluentValidation/Internal/CollectionPropertyRule.cs @@ -61,33 +61,6 @@ public CollectionPropertyRule(MemberInfo member, Func> return new CollectionPropertyRule(member, x => compiled(x), expression, cascadeModeThunk, typeof(TElement)); } - /// - /// Creates a new property rule from a lambda expression. - /// - internal static CollectionPropertyRule CreateTransformed(Expression>> expression, Func transformer, Func cascadeModeThunk, bool bypassCache = false) { - var member = expression.GetMember(); - var compiled = AccessorCache.GetCachedAccessor(member, expression, bypassCache, "FV_RuleForEach"); - - IEnumerable PropertyFunc(T instance) => - compiled(instance).Select(transformer); - - return new CollectionPropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TElement)); - } - - /// - /// Creates a new property rule from a lambda expression. - /// - internal static CollectionPropertyRule CreateTransformed(Expression>> expression, Func transformer, Func cascadeModeThunk, bool bypassCache = false) { - var member = expression.GetMember(); - var compiled = AccessorCache.GetCachedAccessor(member, expression, bypassCache, "FV_RuleForEach"); - - IEnumerable PropertyFunc(T instance) { - return compiled(instance).Select(element => transformer(instance, element)); - } - - return new CollectionPropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TOriginal)); - } - async ValueTask IValidationRuleInternal.ValidateAsync(ValidationContext context, bool useAsync, CancellationToken cancellation) { string displayName = GetDisplayName(context); diff --git a/src/FluentValidation/Internal/ExtensionsInternal.cs b/src/FluentValidation/Internal/ExtensionsInternal.cs index fbe2a3b45..681b41b7c 100644 --- a/src/FluentValidation/Internal/ExtensionsInternal.cs +++ b/src/FluentValidation/Internal/ExtensionsInternal.cs @@ -20,24 +20,19 @@ namespace FluentValidation.Internal; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; +using System.Runtime.CompilerServices; using System.Text; using Resources; internal static class ExtensionsInternal { - internal static void Guard(this object obj, string message, string paramName) { - if (obj == null) { - throw new ArgumentNullException(paramName, message); - } - } - - internal static void Guard(this string str, string message, string paramName) { - if (str == null) { - throw new ArgumentNullException(paramName, message); - } - if (string.IsNullOrEmpty(str)) { - throw new ArgumentException(message, paramName); + // Todo: Replace with ArgumentException.ThrowIfNullOrEmpty once we stop supporting .net 6 + public static void ThrowIfNullOrEmpty([NotNull] string argument, [CallerArgumentExpression("argument")] string paramName = null) { + if (string.IsNullOrEmpty(argument)) { + ArgumentNullException.ThrowIfNull(argument, paramName); + throw new ArgumentException("The value cannot be an empty string", paramName); } } diff --git a/src/FluentValidation/Internal/PropertyRule.cs b/src/FluentValidation/Internal/PropertyRule.cs index 9ae56bf61..6c8e2ee4c 100644 --- a/src/FluentValidation/Internal/PropertyRule.cs +++ b/src/FluentValidation/Internal/PropertyRule.cs @@ -43,32 +43,6 @@ public PropertyRule(MemberInfo member, Func propertyFunc, LambdaEx return new PropertyRule(member, x => compiled(x), expression, cascadeModeThunk, typeof(TProperty)); } - /// - /// Creates a new property rule from a lambda expression. - /// - internal static PropertyRule Create(Expression> expression, Func transformer, Func cascadeModeThunk, bool bypassCache = false) { - var member = expression.GetMember(); - var compiled = AccessorCache.GetCachedAccessor(member, expression, bypassCache); - - TProperty PropertyFunc(T instance) - => transformer(compiled(instance)); - - return new PropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TOld)); - } - - /// - /// Creates a new property rule from a lambda expression. - /// - internal static PropertyRule Create(Expression> expression, Func transformer, Func cascadeModeThunk, bool bypassCache = false) { - var member = expression.GetMember(); - var compiled = AccessorCache.GetCachedAccessor(member, expression, bypassCache); - - TProperty PropertyFunc(T instance) - => transformer(instance, compiled(instance)); - - return new PropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TOld)); - } - /// /// Performs validation using a validation context and adds collected validation failures to the Context. /// diff --git a/src/FluentValidation/Internal/RuleBase.cs b/src/FluentValidation/Internal/RuleBase.cs index a260412f8..2176d46e1 100644 --- a/src/FluentValidation/Internal/RuleBase.cs +++ b/src/FluentValidation/Internal/RuleBase.cs @@ -115,13 +115,7 @@ internal abstract class RuleBase : IValidationRule public CascadeMode CascadeMode { get => _cascadeModeThunk(); - set { -#pragma warning disable 618 - _cascadeModeThunk = value == CascadeMode.StopOnFirstFailure - ? () => CascadeMode.Stop - : () => value; -#pragma warning restore 618 - } + set => _cascadeModeThunk = () => value; } /// @@ -283,6 +277,16 @@ public string GetDisplayName(ValidationContext context) object IValidationRule.GetPropertyValue(T instance) => PropertyFunc(instance); + bool IValidationRule.TryGetPropertyValue(T instance, out TProp value) { + value = default; + + var result = PropertyFunc(instance); + if (result is not TProp propertyValue) return false; + + value = propertyValue; + return true; + } + /// /// Prepares the of for an upcoming . /// diff --git a/src/FluentValidation/Internal/RuleBuilder.cs b/src/FluentValidation/Internal/RuleBuilder.cs index 7b9c79acc..02fb0d1de 100644 --- a/src/FluentValidation/Internal/RuleBuilder.cs +++ b/src/FluentValidation/Internal/RuleBuilder.cs @@ -64,7 +64,7 @@ internal class RuleBuilder : IRuleBuilderOptions, IR } public IRuleBuilderOptions SetValidator(IValidator validator, params string[] ruleSets) { - validator.Guard("Cannot pass a null validator to SetValidator", nameof(validator)); + ArgumentNullException.ThrowIfNull(validator); var adaptor = new ChildValidatorAdaptor(validator, validator.GetType()) { RuleSets = ruleSets }; @@ -74,7 +74,7 @@ internal class RuleBuilder : IRuleBuilderOptions, IR } public IRuleBuilderOptions SetValidator(Func validatorProvider, params string[] ruleSets) where TValidator : IValidator { - validatorProvider.Guard("Cannot pass a null validatorProvider to SetValidator", nameof(validatorProvider)); + ArgumentNullException.ThrowIfNull(validatorProvider); var adaptor = new ChildValidatorAdaptor((context, _) => validatorProvider(context.InstanceToValidate), typeof (TValidator)) { RuleSets = ruleSets }; @@ -84,7 +84,7 @@ internal class RuleBuilder : IRuleBuilderOptions, IR } public IRuleBuilderOptions SetValidator(Func validatorProvider, params string[] ruleSets) where TValidator : IValidator { - validatorProvider.Guard("Cannot pass a null validatorProvider to SetValidator", nameof(validatorProvider)); + ArgumentNullException.ThrowIfNull(validatorProvider); var adaptor = new ChildValidatorAdaptor((context, val) => validatorProvider(context.InstanceToValidate, val), typeof (TValidator)) { RuleSets = ruleSets }; @@ -94,6 +94,16 @@ internal class RuleBuilder : IRuleBuilderOptions, IR } IRuleBuilderOptions IRuleBuilderOptions.DependentRules(Action action) { + DependentRulesInternal(action); + return this; + } + + IRuleBuilderOptionsConditions IRuleBuilderOptionsConditions.DependentRules(Action action) { + DependentRulesInternal(action); + return this; + } + + private void DependentRulesInternal(Action action) { var dependencyContainer = new List>(); // Capture any rules added to the parent validator inside this delegate. using (ParentValidator.Rules.Capture(dependencyContainer.Add)) { @@ -109,10 +119,10 @@ internal class RuleBuilder : IRuleBuilderOptions, IR } Rule.AddDependentRules(dependencyContainer); - return this; } public void AddComponent(RuleComponent component) { Rule.Components.Add(component); } + } diff --git a/src/FluentValidation/Syntax.cs b/src/FluentValidation/Syntax.cs index db6b4ac87..ed0460622 100644 --- a/src/FluentValidation/Syntax.cs +++ b/src/FluentValidation/Syntax.cs @@ -92,6 +92,10 @@ public interface IRuleBuilderOptions : IRuleBuilder /// public interface IRuleBuilderOptionsConditions : IRuleBuilder { + /// + /// Creates a scope for declaring dependent rules. + /// + IRuleBuilderOptionsConditions DependentRules(Action action); } /// diff --git a/src/FluentValidation/TestHelper/ITestValidationContinuation.cs b/src/FluentValidation/TestHelper/ITestValidationContinuation.cs index a3f0a02f7..254ba157c 100644 --- a/src/FluentValidation/TestHelper/ITestValidationContinuation.cs +++ b/src/FluentValidation/TestHelper/ITestValidationContinuation.cs @@ -11,7 +11,7 @@ public interface ITestValidationWith : ITestValidationContinuation { public interface ITestValidationContinuation : IEnumerable { IEnumerable UnmatchedFailures { get; } - //TODO: 12.x expose MatchedFailures on the interface too. + IEnumerable MatchedFailures { get; } } internal class TestValidationContinuation : ITestValidationContinuation, ITestValidationWith { diff --git a/src/FluentValidation/TestHelper/TestValidationResult.cs b/src/FluentValidation/TestHelper/TestValidationResult.cs index 884e4a15e..550a4bcb7 100644 --- a/src/FluentValidation/TestHelper/TestValidationResult.cs +++ b/src/FluentValidation/TestHelper/TestValidationResult.cs @@ -44,6 +44,17 @@ public class TestValidationResult : ValidationResult { ShouldNotHaveValidationError(propertyName, true); } + public void ShouldNotHaveAnyValidationErrors() { + ShouldNotHaveValidationError(ValidationTestExtension.MatchAnyFailure, true); + } + + public ITestValidationContinuation ShouldHaveValidationErrors() { + if (!Errors.Any()) + throw new ValidationTestException($"Expected at least one validation error, but none were found."); + + return new TestValidationContinuation(Errors); + } + public ITestValidationWith ShouldHaveValidationErrorFor(string propertyName) { return ShouldHaveValidationError(propertyName, false); } @@ -52,8 +63,7 @@ public class TestValidationResult : ValidationResult { ShouldNotHaveValidationError(propertyName, false); } - // TODO: Make private in 12.0 - internal ITestValidationWith ShouldHaveValidationError(string propertyName, bool shouldNormalizePropertyName) { + private ITestValidationWith ShouldHaveValidationError(string propertyName, bool shouldNormalizePropertyName) { var result = new TestValidationContinuation(Errors); result.ApplyPredicate(x => (shouldNormalizePropertyName ? NormalizePropertyName(x.PropertyName) == propertyName : x.PropertyName == propertyName) || (string.IsNullOrEmpty(x.PropertyName) && string.IsNullOrEmpty(propertyName)) @@ -82,8 +92,7 @@ public class TestValidationResult : ValidationResult { throw new ValidationTestException(errorMessage); } - // TODO: Make private in 12.0 - internal void ShouldNotHaveValidationError(string propertyName, bool shouldNormalizePropertyName) { + private void ShouldNotHaveValidationError(string propertyName, bool shouldNormalizePropertyName) { var failures = Errors.Where(x => (shouldNormalizePropertyName ? NormalizePropertyName(x.PropertyName) == propertyName : x.PropertyName == propertyName) || (string.IsNullOrEmpty(x.PropertyName) && string.IsNullOrEmpty(propertyName)) || propertyName == ValidationTestExtension.MatchAnyFailure diff --git a/src/FluentValidation/TestHelper/ValidatorTestExtensions.cs b/src/FluentValidation/TestHelper/ValidatorTestExtensions.cs index a30116119..7bce979b8 100644 --- a/src/FluentValidation/TestHelper/ValidatorTestExtensions.cs +++ b/src/FluentValidation/TestHelper/ValidatorTestExtensions.cs @@ -119,19 +119,6 @@ public static class ValidationTestExtension { return new TestValidationResult(validationResult); } - // TODO: 12.0: Move this to an instance method on TestValidationResult - public static ITestValidationContinuation ShouldHaveAnyValidationError(this TestValidationResult testValidationResult) { - if (!testValidationResult.Errors.Any()) - throw new ValidationTestException($"Expected at least one validation error, but none were found."); - - return new TestValidationContinuation(testValidationResult.Errors); - } - - // TODO: 12.0: Move this to an instance method on TestValidationResult - public static void ShouldNotHaveAnyValidationErrors(this TestValidationResult testValidationResult) { - testValidationResult.ShouldNotHaveValidationError(MatchAnyFailure, true); - } - private static string BuildErrorMessage(ValidationFailure failure, string exceptionMessage, string defaultMessage) { if (exceptionMessage != null && failure != null) { var formattedExceptionMessage = exceptionMessage.Replace("{Code}", failure.ErrorCode) @@ -152,8 +139,7 @@ public static class ValidationTestExtension { public static ITestValidationWith When(this ITestValidationContinuation failures, Func failurePredicate, string exceptionMessage = null) { - //TODO 12.0 remove casts. - var result = new TestValidationContinuation(((TestValidationContinuation)failures).MatchedFailures, failures); + var result = new TestValidationContinuation(failures.MatchedFailures, failures); result.ApplyPredicate(failurePredicate); var anyMatched = result.Any(); @@ -167,8 +153,7 @@ public static class ValidationTestExtension { } public static ITestValidationContinuation WhenAll(this ITestValidationContinuation failures, Func failurePredicate, string exceptionMessage = null) { - //TODO 12.0 remove casts. - var result = new TestValidationContinuation(((TestValidationContinuation)failures).MatchedFailures, failures); + var result = new TestValidationContinuation(failures.MatchedFailures, failures); result.ApplyPredicate(failurePredicate); bool allMatched = !result.UnmatchedFailures.Any(); diff --git a/src/FluentValidation/ValidatorOptions.cs b/src/FluentValidation/ValidatorOptions.cs index dfaf9c68e..5f65aa90e 100644 --- a/src/FluentValidation/ValidatorOptions.cs +++ b/src/FluentValidation/ValidatorOptions.cs @@ -38,107 +38,21 @@ public class ValidatorConfiguration { private Func _errorCodeResolver = DefaultErrorCodeResolver; private ILanguageManager _languageManager = new LanguageManager(); - private CascadeMode _defaultClassLevelCascadeMode = CascadeMode.Continue; - private CascadeMode _defaultRuleLevelCascadeMode = CascadeMode.Continue; - - /// - /// - /// Gets a single mode value representing the default values of - /// - /// and ., based on the same logic as used when setting - /// this property as described below. - /// - /// - /// Sets the default values of - /// and . - /// - /// - /// If set to or , then both properties are set - /// to that value by default. - /// - /// - /// If set to the deprecated , - /// then - /// is set to by default, and - /// is set to by default. - /// This results in the same behaviour as before this property was deprecated. - /// - /// - /// Note that cascade mode behaviour within individual rules is controlled by - /// . - /// - /// - [Obsolete($"Use {nameof(DefaultClassLevelCascadeMode)} and/or {nameof(DefaultRuleLevelCascadeMode)} instead. " + - "CascadeMode will be removed in a future release. " + - "For more details, see https://docs.fluentvalidation.net/en/latest/cascade.html")] - public CascadeMode CascadeMode { -#pragma warning disable 618 - get { - if (_defaultClassLevelCascadeMode == _defaultRuleLevelCascadeMode) { - return _defaultClassLevelCascadeMode; - } - else if (_defaultClassLevelCascadeMode == CascadeMode.Continue && _defaultRuleLevelCascadeMode == CascadeMode.Stop) { - return CascadeMode.StopOnFirstFailure; - } - else { - throw new Exception( - $"There is no conversion to a single {nameof(CascadeMode)} value from the current combination of " + - $"{nameof(DefaultClassLevelCascadeMode)} and {nameof(DefaultRuleLevelCascadeMode)}. " + - $"Please use these properties instead of the deprecated {nameof(CascadeMode)} going forward."); - } - } - - set { - DefaultClassLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Continue - : value; - - DefaultRuleLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Stop - : value; -#pragma warning restore 618 - } - } - /// /// /// Sets the default value for . /// Defaults to if not set. /// - /// - /// This cannot be set to the deprecated . - /// . Attempting to do so it will actually - /// result in being used. - /// /// - public CascadeMode DefaultClassLevelCascadeMode { - get => _defaultClassLevelCascadeMode; -#pragma warning disable 618 - set => _defaultClassLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Stop - : value; -#pragma warning restore 618 - } + public CascadeMode DefaultClassLevelCascadeMode { get; set; } = CascadeMode.Continue; /// /// /// Sets the default value for /// Defaults to if not set. /// - /// - /// This cannot be set to the deprecated . - /// . Attempting to do so it will actually - /// result in being used. - /// /// - public CascadeMode DefaultRuleLevelCascadeMode { - get => _defaultRuleLevelCascadeMode; -#pragma warning disable 618 - set => _defaultRuleLevelCascadeMode = value == CascadeMode.StopOnFirstFailure - ? CascadeMode.Stop - : value; -#pragma warning restore 618 - } + public CascadeMode DefaultRuleLevelCascadeMode { get; set; } = CascadeMode.Continue; /// /// Default severity level diff --git a/src/FluentValidation/Validators/AbstractComparisonValidator.cs b/src/FluentValidation/Validators/AbstractComparisonValidator.cs index a26f1d37e..cfd512213 100644 --- a/src/FluentValidation/Validators/AbstractComparisonValidator.cs +++ b/src/FluentValidation/Validators/AbstractComparisonValidator.cs @@ -34,7 +34,7 @@ public abstract class AbstractComparisonValidator : PropertyValida /// /// protected AbstractComparisonValidator(TProperty value) { - value.Guard("value must not be null.", nameof(value)); + ArgumentNullException.ThrowIfNull(value); ValueToCompare = value; } diff --git a/src/FluentValidation/Validators/AsyncPredicateValidator.cs b/src/FluentValidation/Validators/AsyncPredicateValidator.cs index d3b295671..3e5fc64c1 100644 --- a/src/FluentValidation/Validators/AsyncPredicateValidator.cs +++ b/src/FluentValidation/Validators/AsyncPredicateValidator.cs @@ -37,8 +37,8 @@ public class AsyncPredicateValidator : AsyncPropertyValidator /// public AsyncPredicateValidator(Func, CancellationToken, Task> predicate) { - predicate.Guard("A predicate must be specified.", nameof(predicate)); - this._predicate = predicate; + ArgumentNullException.ThrowIfNull(predicate); + _predicate = predicate; } public override Task IsValidAsync(ValidationContext context, TProperty value, CancellationToken cancellation) { diff --git a/src/FluentValidation/Validators/ChildValidatorAdaptor.cs b/src/FluentValidation/Validators/ChildValidatorAdaptor.cs index 8c77da3fc..5a1c7363d 100644 --- a/src/FluentValidation/Validators/ChildValidatorAdaptor.cs +++ b/src/FluentValidation/Validators/ChildValidatorAdaptor.cs @@ -86,8 +86,7 @@ public class ChildValidatorAdaptor : NoopPropertyValidator context, TProperty value) { - context.Guard("Cannot pass a null context to GetValidator", nameof(context)); - + ArgumentNullException.ThrowIfNull(context); return _validatorProvider != null ? _validatorProvider(context, value) : _validator; } diff --git a/src/FluentValidation/Validators/ScalePrecisionValidator.cs b/src/FluentValidation/Validators/PrecisionScaleValidator.cs similarity index 79% rename from src/FluentValidation/Validators/ScalePrecisionValidator.cs rename to src/FluentValidation/Validators/PrecisionScaleValidator.cs index 940109757..045875800 100644 --- a/src/FluentValidation/Validators/ScalePrecisionValidator.cs +++ b/src/FluentValidation/Validators/PrecisionScaleValidator.cs @@ -36,20 +36,30 @@ namespace FluentValidation.Validators; /// 123.4500 has an scale of 4 and a precision of 7, but an effective scale /// and precision of 2 and 5 respectively. /// -public class ScalePrecisionValidator : PropertyValidator { +public class PrecisionScaleValidator : PropertyValidator { - // TODO: For 12.0 swap the parameter order to match the PrecisionScale extension methods and add parameter for IgnoreTrailingZeros. - public ScalePrecisionValidator(int scale, int precision) { - Init(scale, precision); + public PrecisionScaleValidator(int precision, int scale, bool ignoreTrailingZeros) { + Scale = scale; + Precision = precision; + IgnoreTrailingZeros = ignoreTrailingZeros; + + if (Scale < 0) + throw new ArgumentOutOfRangeException(nameof(scale), $"Scale must be a positive integer. [value:{Scale}]."); + + if (Precision < 0) + throw new ArgumentOutOfRangeException(nameof(precision), $"Precision must be a positive integer. [value:{Precision}]."); + + if (Precision < Scale) + throw new ArgumentOutOfRangeException(nameof(scale), $"Scale must be less than precision. [scale:{Scale}, precision:{Precision}]."); } public override string Name => "ScalePrecisionValidator"; - public int Scale { get; set; } + public int Scale { get; } - public int Precision { get; set; } + public int Precision { get; } - public bool IgnoreTrailingZeros { get; set; } + public bool IgnoreTrailingZeros { get; } public override bool IsValid(ValidationContext context, decimal decimalValue) { var scale = GetScale(decimalValue); @@ -68,22 +78,6 @@ public class ScalePrecisionValidator : PropertyValidator { return true; } - private void Init(int scale, int precision) { - Scale = scale; - Precision = precision; - - if (Scale < 0) - throw new ArgumentOutOfRangeException( - nameof(scale), $"Scale must be a positive integer. [value:{Scale}]."); - if (Precision < 0) - throw new ArgumentOutOfRangeException( - nameof(precision), $"Precision must be a positive integer. [value:{Precision}]."); - if (Precision < Scale) - throw new ArgumentOutOfRangeException( - nameof(scale), - $"Scale must be less than precision. [scale:{Scale}, precision:{Precision}]."); - } - private static UInt32[] GetBits(decimal Decimal) { // We want the integer parts as uint // C# doesn't permit int[] to uint[] conversion, but .NET does. This is somewhat evil... diff --git a/src/FluentValidation/Validators/PredicateValidator.cs b/src/FluentValidation/Validators/PredicateValidator.cs index fd5f272b9..bdd3d2e41 100644 --- a/src/FluentValidation/Validators/PredicateValidator.cs +++ b/src/FluentValidation/Validators/PredicateValidator.cs @@ -18,19 +18,17 @@ namespace FluentValidation.Validators; -using Internal; -using Resources; +using System; public class PredicateValidator : PropertyValidator, IPredicateValidator { - public delegate bool Predicate(T instanceToValidate, TProperty propertyValue, ValidationContext propertyValidatorContext); - private readonly Predicate _predicate; + private readonly Func, bool> _predicate; public override string Name => "PredicateValidator"; - public PredicateValidator(Predicate predicate) { - predicate.Guard("A predicate must be specified.", nameof(predicate)); - this._predicate = predicate; + public PredicateValidator(Func, bool> predicate) { + ArgumentNullException.ThrowIfNull(predicate); + _predicate = predicate; } public override bool IsValid(ValidationContext context, TProperty value) {