diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..3e06698e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or sample code about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support-request.md b/.github/ISSUE_TEMPLATE/support-request.md new file mode 100644 index 000000000..cf6fee8d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support-request.md @@ -0,0 +1,21 @@ +--- +name: Support Request +about: Issues, bug reports and general support requests +title: '' +labels: '' +assignees: '' + +--- + + +**Which version of FluentValidation are you using?** +Please specify which version of FluentValidation you're using. + +**Which version of ASP.NET are you using?** +If you are using FluentValidation with ASP.NET, please provide the version of ASP.NET that you're using (eg .NET Core 2.1, NET Core 3.1 etc) + +**Describe the issue that you're having** +A clear and concise description of the issue that you're having, steps to reproduce, and sample code to reproduce the issue. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8a628e4d..2c77026f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,9 +22,9 @@ Please log a new issue in the appropriate GitHub repository: * [Legacy ASP.NET MVC5 / WebApi 2 extensions](https://github.com/FluentValidation/FluentValidation.LegacyWeb) ## Filing issues -The best way to get your bug fixed is to be as detailed as you can be about the problem. +The best way to get your bug fixed is to be as detailed as you can be about the problem. -Please first check the documentation at https://fluentvalidation.net first to see if your question is addressed there. +Please first check the documentation at https://fluentvalidation.net first to see if your question is addressed there. If not, then please provide the exact version of FluentValidation that you're using along with a detailed explanation of the issue and complete steps to reproduce the problem is ideal. Please ensure all sample code is properly formatted and readable (GitHub supports [markdown](https://github.github.com/github-flavored-markdown/)) @@ -36,4 +36,17 @@ Make sure you can build the code. Familiarize yourself with the project workflow If you wish to submit a new feature, please open an issue to discuss it with the project maintainers - don't open a pull request without first discussing whether the feature fits with the project roadmap. -Tests must be provided for all pull requests that add or change functionality. +Tests must be provided for all pull requests that add or change functionality. + +## Building the Docs + +The docs are built separately from the source code. Building the docs requires Python 3 and pip. This is then used to install Sphinx and dependencies, which then enable `make` to build the site. + +For example, on Linux / within WSL: + +* `sudo apt install python3-pip` +* `cd docs` to go to the docs directory +* `pip3 install -r requirements.txt` to install the required packages for the docs +* On WSL, you may need to exit and restart at this point. +* `PATH=$PATH:~/.local/lib/python3.8/site-packages` (you may want to add this to your `.bashrc` file as well.) +* `make html` to build the site or `make serve` to watch for changes. diff --git a/Changelog.txt b/Changelog.txt index 6a4c97092..6fb4de369 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,4 +1,4 @@ -9.0 - +9.0 - 6 July 2020 Removed support for netstandard1.1, netstandard1.6 and net45 (net461 still supported) Removed support for unsupported .NET Core versions (2.2 and 3.0). LTS versions are supported (2.1 and 3.1) Default email validation mode now uses the same logic as ASP.NET Core. Previous regex behaviour is opt-in. @@ -29,6 +29,8 @@ SourceLink integration. Additional ValidationException constructor that allows using both the default message and a custom one together. ScalePrecisionValidator algorithm now matches SQL Server. Additional overload of the When methods that contain the validation context. +Automatically scanned types can be excluded when calling RegisterValidatorsFromAssemblyContaining in ASP.NET Core projects. +New AutomaticValidationEnabled property for use in ASP.NET Core projects (defaults to true). 8.6.2 - 29 February 2020 Fix CollectionIndex placeholder not working with async workflow. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index fe6d4fcff..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ - - -### System Details - -- FluentValidation version: -- Web Framework version (eg ASP.NET Core 2.1, MVC 5, WebApi 2. Delete if not applicable): - -### Issue Description diff --git a/README.md b/README.md index a6c51c761..721550924 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,12 @@ and lambda expressions for building validation rules. FluentValidation can be installed using the Nuget package manager or the `dotnet` CLI. ``` -Install-Package FluentValidation +dotnet add package FluentValidation ``` For ASP.NET Core integration: ``` -Install-Package FluentValidation.AspNetCore -``` - -For legacy ASP.NET MVC/WebApi integration: - -``` -Install-Package FluentValidation.MVC5 -Install-Package FluentValidation.WebApi +dotnet add package FluentValidation.AspNetCore ``` --- diff --git a/docs/aspnet.md b/docs/aspnet.md index 8c4c3807e..eb17958cc 100644 --- a/docs/aspnet.md +++ b/docs/aspnet.md @@ -2,12 +2,13 @@ ### Getting Started -FluentValidation can be integrated with ASP.NET Core. Once enabled, MVC will use FluentValidation to validate objects that are passed in to controller actions by the model binding infrastructure. +FluentValidation supports integration with ASP.NET Core 2.1 or 3.1 (3.1 recommended). Once enabled, MVC will use FluentValidation to validate objects that are passed in to controller actions by the model binding infrastructure. -To enable MVC integration, you'll need to add a reference to the `FluentValidation.AspNetCore` assembly by installing the appropriate NuGet package: + +To enable MVC integration, you'll need to add a reference to the `FluentValidation.AspNetCore` assembly by installing the appropriate NuGet package. From the command line, you can install the package by typing: ```shell -Install-Package FluentValidation.AspNetCore +dotnet add package FluentValidation.AspNetCore ``` Once installed, you'll need to configure FluentValidation in your app's Startup class by calling the `AddFluentValidation` extension method inside the `ConfigureServices` method (which requires a `using FluentValidation.AspNetCore`). This method must be called directly after calling `AddMvc`. @@ -21,7 +22,7 @@ public void ConfigureServices(IServiceCollection services) { } ``` -In order for ASP.NET to discover your validators, they must be registered with the services collection. You can either do this by calling the `AddTransient` method for each of your validators... +In order for ASP.NET to discover your validators, they must be registered with the services collection. You can either do this by calling the `AddTransient` method for each of your validators: ```csharp @@ -35,19 +36,27 @@ public void ConfigureServices(IServiceCollection services) { } ``` -...or by using the `AddFromAssemblyContaining` method to automatically register all validators within a particular assembly. This will automatically find any public, non-abstract types that inherit from `AbstractValidator` and register them with the container (open generics are not supported). +### Automatic Registration + +You can also use the `AddFromAssemblyContaining` method to automatically register all validators within a particular assembly. This will automatically find any public, non-abstract types that inherit from `AbstractValidator` and register them with the container (open generics are not supported). ```csharp services.AddMvc() .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining()); ``` -```eval_rst -.. note:: - Validators that are registered automatically using `RegisterValidationsFromAssemblyContaining` are registered as `Transient` with the container. You can choose to register them as a different lifetime by instead using the extension method `AddValidatorsFromAssemblyContaining`, or by explicitly registering individual validators with the container instead of auto-registering. +Validators that are registered automatically using `RegisterValidationsFromAssemblyContaining` are registered as `Transient` with the container rather than as `Singleton`. This is done to avoid lifecycle scoping issues where a developer may inadvertantly cause a singleton-scoped validator from depending on a Transient or Request-scoped service (for example, a DB context). If you are aware of these kind of issues and understand how to avoid them, then you may choose to regiter the validators as singletons instead, which will give a performance boost by passing in a second argument: `fv.RegisterValidatorsFromAssemblyContaining(lifetime: ServiceLifetime.Singleton)` (note that this optional parameter is only available in FluentValidation 9.0 or newer). + +You can also optionally prevent certain types from being automatically registered when using this approach by passing a filter to `RegisterValidationsFromAssemblyContaining`. For example, if there is a specific validator type that you don't want to be registered, you can use a filter callback to exclude it: + +```csharp +services.AddMvc() + .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining(discoveredType => discoveredType.ValidatorType != typeof(SomeValidatorToExclude))); ``` -This example assumes that the PersonValidator is defined to validate a class called `Person`: +### Using the validator in a controller + +This next example assumes that the `PersonValidator` is defined to validate a class called `Person`. Once you've configured FluentValidation, ASP.NET will then automatically validate incoming requests using the validator mappings configured in your startup routine. ```csharp public class Person { @@ -67,7 +76,6 @@ public class PersonValidator : AbstractValidator { } ``` - We can use the Person class within our controller and associated view: ```csharp @@ -115,7 +123,7 @@ public class PeopleController : Controller { Now when you post the form, MVC's model-binding infrastructure will validate the `Person` object with the `PersonValidator`, and add the validation results to ModelState. -*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](/advanced.html#root-context-data) contains an entry called `InvokedByMvc` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically (by MVC), or manually. +*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](advanced.html#root-context-data) contains an entry called `InvokedByMvc` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically (by MVC), or manually. ### Compatibility with ASP.NET's built-in Validation @@ -133,7 +141,7 @@ services.AddMvc().AddFluentValidation(fv => { ### Implicit vs Explicit Child Property Validation -When validating complex object graphs, by default, you must explicitly specify any child validators for complex properties by using `SetValidator` ([see the section on validating complex properties](/start.html#complex-properties)) +When validating complex object graphs, by default, you must explicitly specify any child validators for complex properties by using `SetValidator` ([see the section on validating complex properties](start.html#complex-properties)) When running an ASP.NET MVC application, you can also optionally enable implicit validation for child properties. When this is enabled, instead of having to specify child validators using `SetValidator`, MVC's validation infrastructure will recursively attempt to automatically find validators for each property. This can be done by setting `ImplicitlyValidateChildProperties` to true: diff --git a/docs/conf.py b/docs/conf.py index bd8fc184a..f324df490 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,14 +34,15 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "recommonmark", ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -source_parsers = { - '.md': CommonMarkParser -} +#source_parsers = { +# '.md': CommonMarkParser +#} # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: diff --git a/docs/configuring.md b/docs/configuring.md index bf2f4856f..62cefbca5 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -38,7 +38,9 @@ It is also possible to use your own custom arguments in the validation message. ```csharp //Using static values in a custom message: -RuleFor(customer => x.Surname).NotNull().WithMessage(customer => string.Format("This message references some constant values: {0} {1}", "hello", 5)); +RuleFor(customer => x.Surname) + .NotNull() + .WithMessage(customer => string.Format("This message references some constant values: {0} {1}", "hello", 5)) //Result would be "This message references some constant values: hello 5" //Referencing other property values: @@ -81,13 +83,4 @@ ValidatorOptions.DisplayNameResolver = (type, member, expression) => { }; ``` -This is not a realistic example as it changes all properties to have the suffix "Foo", but hopefully illustrates the point. - -Additionally, FluentValidation will respect the use of the DisplayName and Display attributes for generating the property's name within error messages: - -```csharp -public class Person { - [Display(Name="Last name")] - public string Surname { get; set; } -} -``` +This is not a realistic example as it changes all properties to have the suffix "Foo", but hopefully illustrates the point. \ No newline at end of file diff --git a/docs/custom-state.md b/docs/custom-state.md new file mode 100644 index 000000000..d00b7fc93 --- /dev/null +++ b/docs/custom-state.md @@ -0,0 +1,33 @@ +# Custom State + +There may be an occasion where you'd like to return contextual information about the state of your validation rule when it was run. The `WithCustomState` method allows you to associate any custom data with the validation results. + +We could assign a custom state by modifying a line to read: + +```csharp +public class PersonValidator : AbstractValidator { + public PersonValidator() { + RuleFor(person => person.Surname).NotNull(); + RuleFor(person => person.Forename).NotNull().WithState(person => 1234); + } +} +``` + +This state is then available within the `CustomState` property of the `ValidationFailure`. + +```csharp +var validator = new PersonValidator(); +var result = validator.Validate(new Person()); +foreach (var failure in result.Errors) { + Console.WriteLine($"Property: {failure.PropertyName} State: {failure.CustomState}"); +} +``` + +The output would be: + +``` +Property: Surname State: +Property: Forename State: 1234 +``` + +By default the `CustomState` property will be `null` if `WithState` hasn't been called. diff --git a/docs/custom-validators.md b/docs/custom-validators.md index bd86d12a8..c9940d5e9 100644 --- a/docs/custom-validators.md +++ b/docs/custom-validators.md @@ -1,6 +1,6 @@ # Custom Validators -There are several ways to create a custom, reusable validator. The recommended way is to make use of the [Predicate Validator](/built-in-validators.html#predicate-validator) to write a custom validation function, but you can also write a custom implementation of the PropertyValidator class. +There are several ways to create a custom, reusable validator. The recommended way is to make use of the [Predicate Validator](built-in-validators.html#predicate-validator) to write a custom validation function, but you can also write a custom implementation of the PropertyValidator class. For these examples, we'll imagine a scenario where you want to create a reusable validator that will ensure a List object contains fewer than 10 items. diff --git a/docs/error-codes.md b/docs/error-codes.md new file mode 100644 index 000000000..f39e58614 --- /dev/null +++ b/docs/error-codes.md @@ -0,0 +1,38 @@ +# Custom Error Codes + +A custom error code can also be associated with validation rules by calling the `WithErrorCode` method: + +```csharp +public class PersonValidator : AbstractValidator { + public PersonValidator() { + RuleFor(person => person.Surname).NotNull().WithErrorCode("ERR1234"); + RuleFor(person => person.Forename).NotNull(); + } +} +``` + +The resulting error code can be obtained from the `ErrorCode` property on the `ValidationFailure`: + +```csharp +var validator = new PersonValidator(); +var result = validator.Validate(new Person()); +foreach (var failure in result.Errors) { + Console.WriteLine($"Property: {failure.PropertyName} Error Code: {failure.ErrorCode}"); +} +``` + +The output would be: + +``` +Property: Surname Error Code: ERR1234 +Property: Forename Error Code: NotNullValidator +``` + +## ErrorCode and Error Messages + +The `ErrorCode` is also used to determine the default error message for a particular validator. At a high level: + +* The error code is used as the lookup key for an error message. For example, a `NotNull()` validator has a default error code of `NotNullValidator`, which used to look up the error messages from the `LanguageManager`. [See the documentation on localization.](localization) +* If you provide an error code, you could also provide a localized message with the name of that error code to create a custom message. +* If you provide an error code but no custom message, the message will fall back to the default message for that validator. You're not required to add a custom message. +* Using `ErrorCode` can also be used to override the default error message. For example, if you use a custom `Must()` validator, but you'd like to reuse the `NotNull()` validator's default error message, you can call `WithErrorCode("NotNullValidator")` to achieve this result. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 30a5ed22c..15508015b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ The following platforms are supported: * .NET Core 2.0+ * `.NET Standard 2.0+ `_ -For automatic validation with ASP.NET, FluentValidation supports ASP.NET Core 2.1+ (3.1 recommended) +For automatic validation with ASP.NET, FluentValidation supports ASP.NET Core 2.1 and 3.1. If you're new to using FluentValidation, check out the :doc:`start` page. @@ -44,7 +44,7 @@ Example :caption: Getting Started installation - start + start collections rulesets including-rules @@ -58,7 +58,9 @@ Example configuring conditions - + severity + error-codes + custom-state .. _validator-docs: .. toctree:: @@ -85,12 +87,12 @@ Example .. _aspnet-docs: .. toctree:: :maxdepth: 1 - :caption: ASP.NET Integration + :caption: ASP.NET Integration aspnet mvc5 webapi - + .. _advanced-docs: .. toctree:: :maxdepth: 1 diff --git a/docs/installation.md b/docs/installation.md index 95486ce30..d8040d14a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -21,8 +21,14 @@ Or using the .net core CLI from a terminal window: dotnet add package FluentValidation ``` -For integration with ASP.NET Core, install the FluentValidation.AspNetCore package: +For integration with ASP.NET Core, install the FluentValidation.AspNetCore package from Visual Studio: ```shell Install-Package FluentValidation.AspNetCore +``` + +or from the command line: + +```shell +dotnet add package FluentValidation.AspNetCore ``` \ No newline at end of file diff --git a/docs/mvc5.md b/docs/mvc5.md index d15e16400..5994129c4 100644 --- a/docs/mvc5.md +++ b/docs/mvc5.md @@ -2,7 +2,7 @@ ```eval_rst .. warning:: - Integration with ASP.NET MVC 5 is deprecated. For an optimal experience, we suggest using FluentValidtation with ASP.NET Core. + Integration with ASP.NET MVC 5 is no longer supported as of FluentValidation 9. Please migrate to ASP.NET Core. ``` ## Getting Started @@ -101,7 +101,7 @@ public class PeopleController : Controller { Now when you post the form MVC’s `DefaultModelBinder` will validate the Person object using the `FluentValidationModelValidatorProvider`. -*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](/advanced.html#root-context-data) contain an entry called `InvokedByMvc` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically by MVC, or manually. +*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](advanced.html#root-context-data) contain an entry called `InvokedByMvc` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically by MVC, or manually. ## Known Limitations diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..e8884d95e --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +-r requirements_rtd.txt +sphinx-autobuild==0.7.1 \ No newline at end of file diff --git a/docs/requirements_rtd.txt b/docs/requirements_rtd.txt new file mode 100644 index 000000000..978d92ab9 --- /dev/null +++ b/docs/requirements_rtd.txt @@ -0,0 +1,3 @@ +recommonmark==0.5.0 +sphinx==1.8.5 +sphinx-rtd-theme==0.4.3 \ No newline at end of file diff --git a/docs/severity.md b/docs/severity.md new file mode 100644 index 000000000..a91019ae2 --- /dev/null +++ b/docs/severity.md @@ -0,0 +1,43 @@ +# Setting the Severity Level + +Given the following example that validates a `Person` object: + +```csharp +public class PersonValidator : AbstractValidator { + public PersonValidator() { + RuleFor(person => person.Surname).NotNull(); + RuleFor(person => person.Forename).NotNull(); + } +} +``` + +By default, if these rules fail they will have a severity of "Error". This can be changed by calling the `WithSeverity` method. For example, if we wanted a missing surname to be identified as a warning instead of an error then we could modify the above line to: + +``` +RuleFor(x => x.Surname).NotNull().WithSeverity(Severity.Warning); +``` + +In version 9.0 and above a callback can be used instead, which also gives you access to the item being validated: + +``` +RuleFor(person => person.Surname).NotNull().WithSeverity(person => Severity.Warning); +``` + +In this case, the `ValidationResult` would still have an `IsValid` result of `false`. However, in the list of `Errors`, the `ValidationFailure` associated with this field will have its `Severity` property set to `Warning`: + +```csharp +var validator = new PersonValidator(); +var result = validator.Validate(new Person()); +foreach (var failure in result.Errors) { + Console.WriteLine($"Property: {failure.PropertyName} Severity: {failure.Severity}"); +} +``` + +The output would be: + +``` +Property: Surname Severity: Warning +Property: Forename Severity: Error +``` + +By default, the severity level of every validation rule is `Error`. Available options are `Error`, `Warning`, or `Info`. \ No newline at end of file diff --git a/docs/upgrading-to-9.md b/docs/upgrading-to-9.md index 6fc995bb3..4201528a4 100644 --- a/docs/upgrading-to-9.md +++ b/docs/upgrading-to-9.md @@ -13,9 +13,9 @@ Support for the following platforms has been dropped: FluentValidation still supports netstandard2 and net461, meaning that it'll run on .NET Core 2.0 or higher (3.1 recommended), or .NET Framework 4.6.1 or higher. -FluentValidation.AspNetCore requires .NET Core 2.0 or higher (3.1 recommended). +FluentValidation.AspNetCore requires .NET Core 2.1 or 3.1 (3.1 recommended). -Integration with MVC5/WebApi 2 is no longer support - both the FluentValidation.Mvc5 and FluentValidation.WebApi packages are deprecated, but will continue to run on .NET Framework 4.6.1 or higher. We recommend migrating to .NET Core as soon as possible. +Integration with MVC5/WebApi 2 is no longer supported - both the FluentValidation.Mvc5 and FluentValidation.WebApi packages were deprecated with the release of FluentValidation 8, but they will now no longer receive further updates. They will continue to run on .NET Framework 4.6.1 or higher, but we recommend migrating to .NET Core as soon as possible. ### Default Email Validation Mode Changed @@ -52,6 +52,19 @@ FluentValidation 4.x-8.x contained a bug where using `NotEqual`/`Equal` on strin [See the documentation for further details.](built-in-validators.html#equal-validator) +### Removal of non-generic Validate overload + +The `IValidator.Validate(object model)` overload has been removed to improve type safety. If you were using this method before, you can use the overload that accepts an `IValidationContext` instead: + +```csharp +var context = new ValidationContext(model); +var result = validator.Validate(context); +``` + +### Removal of non-generic ValidationContext. + +The non-generic `ValidationContext` has been removed. Anywhere that previously used this class will either accept a `ValidationContext` or a non-generic `IValidationContext` interface instead. If you previously made use of this class in custom code, you will need to update it to use one of these as appropriate. + ### Transform updates The `Transform` method can now be used to transform a property value to a different type prior to validation occurring. [See the documentation for further details.](transform) @@ -64,7 +77,7 @@ Prior to 9.0, changing a rule's severity required hard-coding the severity: RuleFor(x => x.Surname).NotNull().WithSeverity(Severity.Warning); ``` -Alternatively, this can be generated from a callback, allowing the severity to be dynamically determined: +Alternatively, this can now be generated from a callback, allowing the severity to be dynamically determined: ```csharp RuleFor(x => x.Surname).NotNull().WithSeverity(x => Severity.Warning); @@ -74,6 +87,10 @@ RuleFor(x => x.Surname).NotNull().WithSeverity(x => Severity.Warning); The algorithm used by the `ScalePrecision` validator has been updated to match SQL Server and other RDBMS systems. The algorithm now correctly checks how many digits are to the left of the decimal point, which it didn't do before. +### ChildValidatorAdaptor and IncludeRule now have generic parameters + +The `ChildvalidatorAdaptor` and `IncludeRule` classes now have generic type parameters. This will not affect users of the public API, but may affect anyone using the internal API. + ### Removed inferring property names from [Display] attribute Older versions of FluentValidation allowed inferring a property's name from the presence of the `[Display]` or `[DisplayName]` attributes on the property. This behaviour has been removed as it causes conflicts with ASP.NET Core's approach to localization using these attributes. diff --git a/docs/webapi.md b/docs/webapi.md index 8a54c929c..040904094 100644 --- a/docs/webapi.md +++ b/docs/webapi.md @@ -2,7 +2,7 @@ ```eval_rst .. warning:: - Integration with ASP.NET WebApi 2 is deprecated. For an optimal experience, we suggest using FluentValidtation with ASP.NET Core. + Integration with ASP.NET WebApi 2 is no longer supported as of FluentValidation 9. Please migrate to ASP.NET Core. ``` ## Getting Started @@ -97,7 +97,7 @@ public class PeopleController : ApiController { Now when you post data to the controller's `Create` method (for example, as JSON) then WebApi will automatically call into FluentValidation to find the corresponding validator. Any validation failures will be stored in the controller's `ModelState` dictionary which can be used to generate an error response which can be returned to the client. -*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](/advanced.html#root-context-data) contain an entry called `InvokedByWebApi` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically by WebApi, or manually. +*Note for advanced users* When validators are executed using this automatic integration, the [RootContextData](advanced.html#root-context-data) contain an entry called `InvokedByWebApi` with a value set to true, which can be used within custom validators to tell whether a validator was invoked automatically by WebApi, or manually. ## Manual Validation diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 93e486ac3..e27c83e08 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ 9.0.0 - preview4 + Jeremy Skinner 7.3 $(NoWarn);1701;1702;1591 diff --git a/src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj b/src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj index 07a1f1295..a9153f85b 100644 --- a/src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj +++ b/src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj @@ -10,6 +10,8 @@ Changes in 9.0.0: * Compatibility with FluentValidation 9.0 * FluentValidationModelValidatorProvider and FluentValidationModelValidator are now public. * Work around a bug in ASP.NET Core's integration testing components that can cause ConfigureServices to run multiple times. +* Automatically scanned types can be excluded when calling RegisterValidatorsFromAssemblyContaining in ASP.NET Core projects. +* New AutomaticValidationEnabled property for use in ASP.NET Core projects (defaults to true). * SourceLink integration * Dropped support for end-of-life .NET Core versions (2.2 and 3.0). LTS versions are supported (2.1 and 3.1) @@ -78,7 +80,7 @@ Full release notes can be found at https://github.com/FluentValidation/FluentVal - + diff --git a/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs b/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs index 8fd4af0d5..a96bbdcbd 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs @@ -81,7 +81,8 @@ public virtual IEnumerable Validate(ModelValidationContex var interceptor = customizations.GetInterceptor() ?? validator as IValidatorInterceptor ?? mvContext.ActionContext.HttpContext.RequestServices.GetService(); - var context = new ValidationContext(mvContext.Model, new PropertyChain(), selector); + + IValidationContext context = new ValidationContext(mvContext.Model, new PropertyChain(), selector); context.RootContextData["InvokedByMvc"] = true; context.SetServiceProvider(mvContext.ActionContext.HttpContext.RequestServices); diff --git a/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs b/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs index cdcb2fdb1..70e21cd61 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs @@ -20,6 +20,7 @@ namespace FluentValidation.AspNetCore { using System; using System.Collections.Generic; using System.Reflection; + using Microsoft.Extensions.DependencyInjection; /// /// FluentValidation asp.net core configuration @@ -63,39 +64,62 @@ public bool LocalizationEnabled { /// public bool ImplicitlyValidateChildProperties { get; set; } + internal bool ClientsideEnabled = true; internal Action ClientsideConfig = x => {}; internal List AssembliesToRegister { get; } = new List(); + internal Func TypeFilter { get; set; } + internal ServiceLifetime ServiceLifetime { get; set; } = ServiceLifetime.Transient; + + /// + /// Whether automatic server-side validation should be enabled (default true). + /// + public bool AutomaticValidationEnabled { get; set; } = true; /// /// Registers all validators derived from AbstractValidator within the assembly containing the specified type /// - public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining() { - return RegisterValidatorsFromAssemblyContaining(typeof(T)); + /// Optional filter that allows certain types to be skipped from registration. + /// The service lifetime that should be used for the validator registration. Defaults to Transient + public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining(Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Transient) { + return RegisterValidatorsFromAssemblyContaining(typeof(T), filter, lifetime); } /// /// Registers all validators derived from AbstractValidator within the assembly containing the specified type /// - public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining(Type type) { - return RegisterValidatorsFromAssembly(type.GetTypeInfo().Assembly); + /// The type that indicates which assembly that should be scanned + /// Optional filter that allows certain types to be skipped from registration. + /// The service lifetime that should be used for the validator registration. Defaults to Transient + public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining(Type type, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Transient) { + return RegisterValidatorsFromAssembly(type.GetTypeInfo().Assembly, filter, lifetime); } /// /// Registers all validators derived from AbstractValidator within the specified assembly /// - public FluentValidationMvcConfiguration RegisterValidatorsFromAssembly(Assembly assembly) { + /// The assembly to scan + /// Optional filter that allows certain types to be skipped from registration. + /// The service lifetime that should be used for the validator registration. Defaults to Transient + public FluentValidationMvcConfiguration RegisterValidatorsFromAssembly(Assembly assembly, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Transient) { ValidatorFactoryType = typeof(ServiceProviderValidatorFactory); AssembliesToRegister.Add(assembly); + TypeFilter = filter; + ServiceLifetime = lifetime; return this; } /// /// Registers all validators derived from AbstractValidator within the specified assemblies /// - public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblies(IEnumerable assemblies) { + /// The assemblies to scan + /// Optional filter that allows certain types to be skipped from registration. + /// The service lifetime that should be used for the validator registration. Defaults to Transient + public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblies(IEnumerable assemblies, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Transient) { ValidatorFactoryType = typeof(ServiceProviderValidatorFactory); AssembliesToRegister.AddRange(assemblies); + TypeFilter = filter; + ServiceLifetime = lifetime; return this; } diff --git a/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs b/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs index 7b195c90e..4718f7c8c 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs @@ -44,8 +44,6 @@ public static IMvcCoreBuilder AddFluentValidation(this IMvcCoreBuilder mvcBuilde var config = new FluentValidationMvcConfiguration(ValidatorOptions.Global); configurationExpression?.Invoke(config); - mvcBuilder.Services.AddValidatorsFromAssemblies(config.AssembliesToRegister); - RegisterServices(mvcBuilder.Services, config); mvcBuilder.AddMvcOptions(options => { @@ -78,19 +76,27 @@ public static IMvcBuilder AddFluentValidation(this IMvcBuilder mvcBuilder, Actio var config = new FluentValidationMvcConfiguration(ValidatorOptions.Global); configurationExpression?.Invoke(config); - mvcBuilder.Services.AddValidatorsFromAssemblies(config.AssembliesToRegister); - RegisterServices(mvcBuilder.Services, config); mvcBuilder.AddMvcOptions(options => { - options.ModelMetadataDetailsProviders.Add(new FluentValidationBindingMetadataProvider()); - options.ModelValidatorProviders.Insert(0, new FluentValidationModelValidatorProvider(config.ImplicitlyValidateChildProperties)); + // Check if the providers have already been added. + // We shouldn't have to do this, but there's a bug in the ASP.NET Core integration + // testing components that can cause Configureservices to be called multple times + // meaning we end up with duplicates. + if (!options.ModelMetadataDetailsProviders.Any(x => x is FluentValidationBindingMetadataProvider)) { + options.ModelMetadataDetailsProviders.Add(new FluentValidationBindingMetadataProvider()); + } + + if (!options.ModelValidatorProviders.Any(x => x is FluentValidationModelValidatorProvider)) { + options.ModelValidatorProviders.Insert(0, new FluentValidationModelValidatorProvider(config.ImplicitlyValidateChildProperties)); + } }); return mvcBuilder; } private static void RegisterServices(IServiceCollection services, FluentValidationMvcConfiguration config) { + services.AddValidatorsFromAssemblies(config.AssembliesToRegister, config.ServiceLifetime, config.TypeFilter); services.AddSingleton(config.ValidatorOptions); if (config.ValidatorFactory != null) { @@ -102,11 +108,13 @@ private static void RegisterServices(IServiceCollection services, FluentValidati services.Add(ServiceDescriptor.Transient(typeof(IValidatorFactory), config.ValidatorFactoryType ?? typeof(ServiceProviderValidatorFactory))); } - services.Add(ServiceDescriptor.Singleton(s => { - var options = s.GetRequiredService>().Value; - var metadataProvider = s.GetRequiredService(); - return new FluentValidationObjectModelValidator(metadataProvider, options.ModelValidatorProviders, config.RunDefaultMvcValidationAfterFluentValidationExecutes); - })); + if (config.AutomaticValidationEnabled) { + services.Add(ServiceDescriptor.Singleton(s => { + var options = s.GetRequiredService>().Value; + var metadataProvider = s.GetRequiredService(); + return new FluentValidationObjectModelValidator(metadataProvider, options.ModelValidatorProviders, config.RunDefaultMvcValidationAfterFluentValidationExecutes); + })); + } if (config.ClientsideEnabled) { // Clientside validation requires access to the HttpContext, but MVC's clientside API does not provide it, diff --git a/src/FluentValidation.AspNetCore/IValidatorInterceptor.cs b/src/FluentValidation.AspNetCore/IValidatorInterceptor.cs index 22fae2aa8..59de92199 100644 --- a/src/FluentValidation.AspNetCore/IValidatorInterceptor.cs +++ b/src/FluentValidation.AspNetCore/IValidatorInterceptor.cs @@ -30,18 +30,18 @@ public interface IValidatorInterceptor { /// It should return a ValidationContext object. /// /// Controller Context - /// Validation Context + /// Validation Context /// Validation Context - ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext); + IValidationContext BeforeMvcValidation(ControllerContext controllerContext, IValidationContext commonContext); /// /// Invoked after MVC validation takes place which allows the result to be customized. /// It should return a ValidationResult. /// /// Controller Context - /// Validation Context + /// Validation Context /// The result of validation. /// Validation Context - ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result); + ValidationResult AfterMvcValidation(ControllerContext controllerContext, IValidationContext commonContext, ValidationResult result); } } diff --git a/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs b/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs index 6140aa50e..471935ced 100644 --- a/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs +++ b/src/FluentValidation.DependencyInjectionExtensions/DependencyInjectionExtensions.cs @@ -34,8 +34,8 @@ public static class DependencyInjectionExtensions { /// /// /// - public static IServiceProvider GetServiceProvider(this IValidationContext context) { - ValidationContext actualContext = null; + public static IServiceProvider GetServiceProvider(this ICommonContext context) { + IValidationContext actualContext = null; switch (context) { case CustomContext cc: @@ -47,7 +47,7 @@ public static IServiceProvider GetServiceProvider(this IValidationContext contex case PropertyValidatorContext pvc: actualContext = pvc.ParentContext; break; - case ValidationContext vc: + case IValidationContext vc: actualContext = vc; break; } @@ -69,7 +69,7 @@ public static IServiceProvider GetServiceProvider(this IValidationContext contex /// /// /// - public static void SetServiceProvider(this ValidationContext context, IServiceProvider serviceProvider) { + public static void SetServiceProvider(this IValidationContext context, IServiceProvider serviceProvider) { context.RootContextData["_FV_ServiceProvider"] = serviceProvider; } @@ -95,7 +95,7 @@ public static IRuleBuilderOptions InjectValidator(th /// /// public static IRuleBuilderOptions InjectValidator(this IRuleBuilder ruleBuilder, Func, IValidator> callback, params string[] ruleSets) { - var adaptor = new ChildValidatorAdaptor(context => { + var adaptor = new ChildValidatorAdaptor(context => { var actualContext = (PropertyValidatorContext) context; var serviceProvider = actualContext.ParentContext.GetServiceProvider(); var contextToUse = ValidationContext.GetFromNonGenericContext(actualContext.ParentContext); diff --git a/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs b/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs index 354043cd2..3a36d57bc 100644 --- a/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs +++ b/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs @@ -29,10 +29,11 @@ public static class ServiceCollectionExtensions { /// The collection of services /// The assemblies to scan /// The lifetime of the validators. The default is transient + /// Optional filter that allows certain types to be skipped from registration. /// - public static IServiceCollection AddValidatorsFromAssemblies(this IServiceCollection services, IEnumerable assemblies, ServiceLifetime lifetime = ServiceLifetime.Transient) { + public static IServiceCollection AddValidatorsFromAssemblies(this IServiceCollection services, IEnumerable assemblies, ServiceLifetime lifetime = ServiceLifetime.Transient, Func filter = null) { foreach (var assembly in assemblies) - services.AddValidatorsFromAssembly(assembly, lifetime); + services.AddValidatorsFromAssembly(assembly, lifetime, filter); return services; } @@ -43,11 +44,12 @@ public static IServiceCollection AddValidatorsFromAssemblies(this IServiceCollec /// The collection of services /// The assembly to scan /// The lifetime of the validators. The default is transient + /// Optional filter that allows certain types to be skipped from registration. /// - public static IServiceCollection AddValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, ServiceLifetime lifetime = ServiceLifetime.Transient) { + public static IServiceCollection AddValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, ServiceLifetime lifetime = ServiceLifetime.Transient, Func filter = null) { AssemblyScanner .FindValidatorsInAssembly(assembly) - .ForEach(scanResult => services.AddScanResult(scanResult, lifetime)); + .ForEach(scanResult => services.AddScanResult(scanResult, lifetime, filter)); return services; } @@ -58,18 +60,20 @@ public static IServiceCollection AddValidatorsFromAssembly(this IServiceCollecti /// The collection of services /// The type whose assembly to scan /// The lifetime of the validators. The default is transient + /// Optional filter that allows certain types to be skipped from registration. /// - public static IServiceCollection AddValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, ServiceLifetime lifetime = ServiceLifetime.Transient) - => services.AddValidatorsFromAssembly(type.Assembly, lifetime); + public static IServiceCollection AddValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, ServiceLifetime lifetime = ServiceLifetime.Transient, Func filter = null) + => services.AddValidatorsFromAssembly(type.Assembly, lifetime, filter); /// /// Adds all validators in the assembly of the type specified by the generic parameter /// /// The collection of services /// The lifetime of the validators. The default is transient + /// Optional filter that allows certain types to be skipped from registration. /// - public static IServiceCollection AddValidatorsFromAssemblyContaining(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient) - => services.AddValidatorsFromAssembly(typeof(T).Assembly, lifetime); + public static IServiceCollection AddValidatorsFromAssemblyContaining(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient, Func filter = null) + => services.AddValidatorsFromAssembly(typeof(T).Assembly, lifetime, filter); /// /// Helper method to register a validator from an AssemblyScanner result @@ -77,21 +81,25 @@ public static IServiceCollection AddValidatorsFromAssemblyContaining(this ISe /// The collection of services /// The scan result /// The lifetime of the validators. The default is transient + /// Optional filter that allows certain types to be skipped from registration. /// - private static IServiceCollection AddScanResult(this IServiceCollection services, AssemblyScanner.AssemblyScanResult scanResult, ServiceLifetime lifetime) { - //Register as interface - services.Add( - new ServiceDescriptor( - serviceType: scanResult.InterfaceType, - implementationType: scanResult.ValidatorType, - lifetime: lifetime)); + private static IServiceCollection AddScanResult(this IServiceCollection services, AssemblyScanner.AssemblyScanResult scanResult, ServiceLifetime lifetime, Func filter) { + bool shouldRegister = filter?.Invoke(scanResult) ?? true; + if (shouldRegister) { + //Register as interface + services.Add( + new ServiceDescriptor( + serviceType: scanResult.InterfaceType, + implementationType: scanResult.ValidatorType, + lifetime: lifetime)); - //Register as self - services.Add( - new ServiceDescriptor( - serviceType: scanResult.ValidatorType, - implementationType: scanResult.ValidatorType, - lifetime: lifetime)); + //Register as self + services.Add( + new ServiceDescriptor( + serviceType: scanResult.ValidatorType, + implementationType: scanResult.ValidatorType, + lifetime: lifetime)); + } return services; } diff --git a/src/FluentValidation.Tests.AspNetCore/FluentValidation.Tests.AspNetCore.csproj b/src/FluentValidation.Tests.AspNetCore/FluentValidation.Tests.AspNetCore.csproj index 2eae1555f..b721746a2 100644 --- a/src/FluentValidation.Tests.AspNetCore/FluentValidation.Tests.AspNetCore.csproj +++ b/src/FluentValidation.Tests.AspNetCore/FluentValidation.Tests.AspNetCore.csproj @@ -14,9 +14,7 @@ - + diff --git a/src/FluentValidation.Tests.AspNetCore/TestModels.cs b/src/FluentValidation.Tests.AspNetCore/TestModels.cs index 8ac1a8876..f6e1896b3 100644 --- a/src/FluentValidation.Tests.AspNetCore/TestModels.cs +++ b/src/FluentValidation.Tests.AspNetCore/TestModels.cs @@ -7,8 +7,8 @@ using FluentValidation.Attributes; using FluentValidation.AspNetCore; using FluentValidation.Results; + using Internal; using Microsoft.AspNetCore.Mvc; - using ValidationContext = FluentValidation.ValidationContext; using ValidationResult = Results.ValidationResult; [Validator(typeof(TestModel5Validator))] @@ -28,22 +28,22 @@ public TestModel5Validator() { public class SimplePropertyInterceptor : FluentValidation.AspNetCore.IValidatorInterceptor { readonly string[] properties = new[] {"Surname", "Forename"}; - public ValidationContext BeforeMvcValidation(ControllerContext cc, ValidationContext context) { - var newContext = context.Clone(selector: new FluentValidation.Internal.MemberNameValidatorSelector(properties)); + public IValidationContext BeforeMvcValidation(ControllerContext cc, IValidationContext context) { + var newContext = new ValidationContext(context.InstanceToValidate, context.PropertyChain, new FluentValidation.Internal.MemberNameValidatorSelector(properties)); return newContext; } - public ValidationResult AfterMvcValidation(ControllerContext cc, ValidationContext context, ValidationResult result) { + public ValidationResult AfterMvcValidation(ControllerContext cc, IValidationContext context, ValidationResult result) { return result; } } public class ClearErrorsInterceptor : FluentValidation.AspNetCore.IValidatorInterceptor { - public ValidationContext BeforeMvcValidation(ControllerContext cc, ValidationContext context) { + public IValidationContext BeforeMvcValidation(ControllerContext cc, IValidationContext context) { return null; } - public ValidationResult AfterMvcValidation(ControllerContext cc, ValidationContext context, ValidationResult result) { + public ValidationResult AfterMvcValidation(ControllerContext cc, IValidationContext context, ValidationResult result) { return new ValidationResult(); } } @@ -62,11 +62,11 @@ public PropertiesValidator2() { RuleFor(x => x.Forename).NotEqual("foo"); } - public ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext) { - return validationContext; + public IValidationContext BeforeMvcValidation(ControllerContext controllerContext, IValidationContext commonContext) { + return commonContext; } - public ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result) { + public ValidationResult AfterMvcValidation(ControllerContext controllerContext, IValidationContext commonContext, ValidationResult result) { return new ValidationResult(); //empty errors } } diff --git a/src/FluentValidation.Tests.AspNetCore/TypeFilterTests.cs b/src/FluentValidation.Tests.AspNetCore/TypeFilterTests.cs new file mode 100644 index 000000000..2974d8ce6 --- /dev/null +++ b/src/FluentValidation.Tests.AspNetCore/TypeFilterTests.cs @@ -0,0 +1,75 @@ +#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.Net.Http; + using System.Threading.Tasks; + using AspNetCore; + using AspNetCore.Controllers; + using Microsoft.Extensions.DependencyInjection; + using Xunit; + using Xunit.Abstractions; + + public class TypeFilterTests : IClassFixture { + private WebAppFixture _webApp; + + public TypeFilterTests(ITestOutputHelper output, WebAppFixture webApp) { + _webApp = webApp; + } + + [Fact] + public async Task Finds_and_executes_validator() { + var client = _webApp.WithFluentValidation(fv => { + fv.RegisterValidatorsFromAssemblyContaining(); + }).CreateClient(); + + var result = await client.GetErrors("InjectsExplicitChildValidator", new FormData()); + + // Validator was found and executed so field shouldn't be valid. + result.IsValidField("Child.Name").ShouldBeFalse(); + + } + + [Fact] + public async Task Filters_types() { + var client = _webApp.WithFluentValidation(fv => { + fv.RegisterValidatorsFromAssemblyContaining(scanResult => { + return scanResult.ValidatorType != typeof(InjectsExplicitChildValidator); + }); + }).CreateClient(); + + var result = await client.GetErrors("InjectsExplicitChildValidator", new FormData()); + + // Should be valid as the validator was skipped. + result.IsValidField("Child.Name").ShouldBeTrue(); + } + + [Fact] + public async Task Disables_automatic_validation() { + var client = _webApp.WithFluentValidation(fv => { + fv.RegisterValidatorsFromAssemblyContaining(); + fv.AutomaticValidationEnabled = false; + }).CreateClient(); + + var result = await client.GetErrors("InjectsExplicitChildValidator", new FormData()); + + // Should be valid as automatic validation is completely disabled.. + result.IsValidField("Child.Name").ShouldBeTrue(); + } + } +} diff --git a/src/FluentValidation.Tests/AbstractValidatorTester.cs b/src/FluentValidation.Tests/AbstractValidatorTester.cs index 04dc755ea..8eec41056 100644 --- a/src/FluentValidation.Tests/AbstractValidatorTester.cs +++ b/src/FluentValidation.Tests/AbstractValidatorTester.cs @@ -237,7 +237,7 @@ public void Validates_type_when_using_non_generic_validate_overload() { IValidator nonGenericValidator = validator; typeof(InvalidOperationException).ShouldBeThrownBy(() => - nonGenericValidator.Validate("foo")); + nonGenericValidator.Validate(new ValidationContext("foo"))); } [Fact] diff --git a/src/FluentValidation.Tests/EqualValidatorTests.cs b/src/FluentValidation.Tests/EqualValidatorTests.cs index 6ce55bd91..94bc857b9 100644 --- a/src/FluentValidation.Tests/EqualValidatorTests.cs +++ b/src/FluentValidation.Tests/EqualValidatorTests.cs @@ -67,6 +67,7 @@ public void Should_store_property_to_compare() { propertyValidator.MemberToCompare.ShouldEqual(typeof(Person).GetProperty("Surname")); } + [Fact] public void Should_store_comparison_type() { var validator = new TestValidator { v => v.RuleFor(x => x.Surname).Equal("Foo") }; @@ -78,9 +79,25 @@ public void Should_store_comparison_type() { [Fact] public void Validates_against_property() { - var validator = new TestValidator { v => v.RuleFor(x => x.Surname).Equal(x => x.Forename) }; - var result = validator.Validate(new Person { Surname = "foo", Forename = "foo" }); - result.IsValid.ShouldBeTrue(); + var validator = new TestValidator {v => v.RuleFor(x => x.Surname).Equal(x => x.Forename).WithMessage("{ComparisonProperty}")}; + var result = validator.Validate(new Person {Surname = "foo", Forename = "bar"}); + result.IsValid.ShouldBeFalse(); + result.Errors[0].ErrorMessage.ShouldEqual("Forename"); + } + + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + var validator = new TestValidator {v => v.RuleFor(x => x.Surname).Equal(x => x.Forename).WithMessage("{ComparisonProperty}")}; + var result = validator.Validate(new Person {Surname = "foo", Forename = "bar"}); + result.Errors[0].ErrorMessage.ShouldEqual("ForenameFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } } [Fact] diff --git a/src/FluentValidation.Tests/ForEachRuleTests.cs b/src/FluentValidation.Tests/ForEachRuleTests.cs index 02c5d04d5..1edf83fdc 100644 --- a/src/FluentValidation.Tests/ForEachRuleTests.cs +++ b/src/FluentValidation.Tests/ForEachRuleTests.cs @@ -148,7 +148,7 @@ class request { } private class MyAsyncNotNullValidator : NotNullValidator { - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { return context.IsAsync(); } } diff --git a/src/FluentValidation.Tests/GreaterThanOrEqualToValidatorTester.cs b/src/FluentValidation.Tests/GreaterThanOrEqualToValidatorTester.cs index 20d8e1e0c..1654f1e03 100644 --- a/src/FluentValidation.Tests/GreaterThanOrEqualToValidatorTester.cs +++ b/src/FluentValidation.Tests/GreaterThanOrEqualToValidatorTester.cs @@ -61,9 +61,25 @@ public void Should_set_default_error_when_validation_fails() { [Fact] public void Validates_with_property() { - var validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThanOrEqualTo(x => x.AnotherInt)); + var validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThanOrEqualTo(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); var result = validator.Validate(new Person { Id = 0, AnotherInt = 1 }); result.IsValid.ShouldBeFalse(); + result.Errors[0].ErrorMessage.ShouldEqual("Another Int"); + } + + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + var validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThanOrEqualTo(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); + var result = validator.Validate(new Person { Id = 0, AnotherInt = 1 }); + result.Errors[0].ErrorMessage.ShouldEqual("AnotherIntFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } } [Fact] diff --git a/src/FluentValidation.Tests/GreaterThanValidatorTester.cs b/src/FluentValidation.Tests/GreaterThanValidatorTester.cs index 116a72df4..4ba2e1949 100644 --- a/src/FluentValidation.Tests/GreaterThanValidatorTester.cs +++ b/src/FluentValidation.Tests/GreaterThanValidatorTester.cs @@ -63,9 +63,25 @@ public void Should_set_default_error_when_validation_fails() { [Fact] public void Validates_with_property() { - validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThan(x => x.AnotherInt)); + validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThan(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); var result = validator.Validate(new Person { Id = 0, AnotherInt = 1 }); result.IsValid.ShouldBeFalse(); + result.Errors[0].ErrorMessage.ShouldEqual("Another Int"); + } + + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + validator = new TestValidator(v => v.RuleFor(x => x.Id).GreaterThan(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); + var result = validator.Validate(new Person { Id = 0, AnotherInt = 1 }); + result.Errors[0].ErrorMessage.ShouldEqual("AnotherIntFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } } [Fact] diff --git a/src/FluentValidation.Tests/LanguageManagerTests.cs b/src/FluentValidation.Tests/LanguageManagerTests.cs index 183843328..6e4ae58bb 100644 --- a/src/FluentValidation.Tests/LanguageManagerTests.cs +++ b/src/FluentValidation.Tests/LanguageManagerTests.cs @@ -194,7 +194,7 @@ public void Uses_error_code_as_localization_key() { public void Falls_back_to_default_localization_key_when_error_code_key_not_found() { var originalLanguageManager = ValidatorOptions.LanguageManager; ValidatorOptions.LanguageManager = new CustomLanguageManager(); - + ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US"); var validator = new InlineValidator(); validator.RuleFor(x => x.Forename).NotNull().WithErrorCode("DoesNotExist"); var result = validator.Validate(new Person()); diff --git a/src/FluentValidation.Tests/LessThanOrEqualToValidatorTester.cs b/src/FluentValidation.Tests/LessThanOrEqualToValidatorTester.cs index 552b618aa..d21aecefe 100644 --- a/src/FluentValidation.Tests/LessThanOrEqualToValidatorTester.cs +++ b/src/FluentValidation.Tests/LessThanOrEqualToValidatorTester.cs @@ -66,9 +66,25 @@ public void Comparison_type() { [Fact] public void Validates_with_property() { - validator = new TestValidator(v => v.RuleFor(x => x.Id).LessThanOrEqualTo(x => x.AnotherInt)); + validator = new TestValidator(v => v.RuleFor(x => x.Id).LessThanOrEqualTo(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); var result = validator.Validate(new Person {Id = 1, AnotherInt = 0}); result.IsValid.ShouldBeFalse(); + result.Errors[0].ErrorMessage.ShouldEqual("Another Int"); + } + + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + validator = new TestValidator(v => v.RuleFor(x => x.Id).LessThanOrEqualTo(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); + var result = validator.Validate(new Person {Id = 1, AnotherInt = 0}); + result.Errors[0].ErrorMessage.ShouldEqual("AnotherIntFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } } [Fact] diff --git a/src/FluentValidation.Tests/LessThanValidatorTester.cs b/src/FluentValidation.Tests/LessThanValidatorTester.cs index 714636fe4..5cbe7676e 100644 --- a/src/FluentValidation.Tests/LessThanValidatorTester.cs +++ b/src/FluentValidation.Tests/LessThanValidatorTester.cs @@ -71,6 +71,21 @@ public void Validates_against_property() { result.Errors[0].ErrorMessage.ShouldEqual("Another Int"); } + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + var validator = new TestValidator(v => v.RuleFor(x => x.Id).LessThan(x => x.AnotherInt).WithMessage("{ComparisonProperty}")); + var result = validator.Validate(new Person { Id = 2, AnotherInt = 1 }); + result.Errors[0].ErrorMessage.ShouldEqual("AnotherIntFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } + } + [Fact] public void Should_throw_when_value_to_compare_is_null() { Expression> nullExpression = null; diff --git a/src/FluentValidation.Tests/NotEqualValidatorTests.cs b/src/FluentValidation.Tests/NotEqualValidatorTests.cs index 3c5816832..789c9fea2 100644 --- a/src/FluentValidation.Tests/NotEqualValidatorTests.cs +++ b/src/FluentValidation.Tests/NotEqualValidatorTests.cs @@ -57,13 +57,35 @@ public void When_the_validator_fails_the_error_message_should_be_set() { [Fact] public void Validates_across_properties() { var validator = new TestValidator( - v => v.RuleFor(x => x.Forename).NotEqual(x => x.Surname) + v => v.RuleFor(x => x.Forename) + .NotEqual(x => x.Surname) + .WithMessage("{ComparisonProperty}") ); var result = validator.Validate(new Person { Surname = "foo", Forename = "foo" }); result.IsValid.ShouldBeFalse(); + result.Errors[0].ErrorMessage.ShouldEqual("Surname"); } + [Fact] + public void Comparison_property_uses_custom_resolver() { + var originalResolver = ValidatorOptions.Global.DisplayNameResolver; + + try { + ValidatorOptions.Global.DisplayNameResolver = (type, member, expr) => member.Name + "Foo"; + var validator = new TestValidator( + v => v.RuleFor(x => x.Forename) + .NotEqual(x => x.Surname) + .WithMessage("{ComparisonProperty}") + ); + + var result = validator.Validate(new Person { Surname = "foo", Forename = "foo" }); + result.Errors[0].ErrorMessage.ShouldEqual("SurnameFoo"); + } + finally { + ValidatorOptions.Global.DisplayNameResolver = originalResolver; + } + } [Fact] public void Should_store_property_to_compare() { diff --git a/src/FluentValidation.Tests/RuleBuilderTests.cs b/src/FluentValidation.Tests/RuleBuilderTests.cs index b74337a62..ac30ae968 100644 --- a/src/FluentValidation.Tests/RuleBuilderTests.cs +++ b/src/FluentValidation.Tests/RuleBuilderTests.cs @@ -161,7 +161,7 @@ public async Task Calling_ValidateAsync_should_delegate_to_underlying_async_vali tcs.SetResult(Enumerable.Empty()); var validator = new Mock(MockBehavior.Loose, ValidatorOptions.LanguageManager.GetStringForValidator()) {CallBase = true}; - validator.Setup(x => x.ShouldValidateAsynchronously(It.IsAny())).Returns(true); + validator.Setup(x => x.ShouldValidateAsynchronously(It.IsAny())).Returns(true); validator.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())).Returns(tcs.Task); builder.SetValidator(validator.Object); @@ -234,7 +234,7 @@ public void Conditional_child_validator_should_register_with_validator_type_not_ var builder = new RuleBuilder(PropertyRule.Create(x => x.Address),null); builder.SetValidator((Person person) => new NoopAddressValidator()); - builder.Rule.Validators.OfType().Single().ValidatorType.ShouldEqual(typeof(NoopAddressValidator)); + builder.Rule.Validators.OfType().Single().ValidatorType.ShouldEqual(typeof(NoopAddressValidator)); } class NoopAddressValidator : AbstractValidator
{ diff --git a/src/FluentValidation.Tests/StandalonePropertyValidationTester.cs b/src/FluentValidation.Tests/StandalonePropertyValidationTester.cs index 6c0edc422..80c6f2798 100644 --- a/src/FluentValidation.Tests/StandalonePropertyValidationTester.cs +++ b/src/FluentValidation.Tests/StandalonePropertyValidationTester.cs @@ -10,7 +10,7 @@ public class StandalonePropertyValidationTester { [Fact] public void Should_validate_property_value_without_instance() { var validator = new NotNullValidator(); - var parentContext = new ValidationContext(null); + var parentContext = new ValidationContext(null); var rule = new PropertyRule(null, x => null, null, null, typeof(string), null) { PropertyName = "Surname" }; diff --git a/src/FluentValidation.Tests/ValidatorTesterTester.cs b/src/FluentValidation.Tests/ValidatorTesterTester.cs index ff0be5ea1..7396265d5 100644 --- a/src/FluentValidation.Tests/ValidatorTesterTester.cs +++ b/src/FluentValidation.Tests/ValidatorTesterTester.cs @@ -39,6 +39,7 @@ public ValidatorTesterTester() { validator.RuleFor(x => x.CreditCard).Must(creditCard => !string.IsNullOrEmpty(creditCard)).WhenAsync((x, cancel) => Task.Run(() => { return x.Age >= 18; })); validator.RuleFor(x => x.Forename).NotNull(); validator.RuleForEach(person => person.NickNames).MinimumLength(5); + CultureScope.SetDefaultCulture(); } [Fact] diff --git a/src/FluentValidation/AbstractValidator.cs b/src/FluentValidation/AbstractValidator.cs index 91e860156..6d93da170 100644 --- a/src/FluentValidation/AbstractValidator.cs +++ b/src/FluentValidation/AbstractValidator.cs @@ -43,20 +43,12 @@ public CascadeMode CascadeMode { set => _cascadeMode = () => value; } - ValidationResult IValidator.Validate(object instance) { - return ((IValidator) this).Validate(new ValidationContext(instance)); - } - - Task IValidator.ValidateAsync(object instance, CancellationToken cancellation) { - return ((IValidator)this).ValidateAsync(new ValidationContext(instance), cancellation); - } - - ValidationResult IValidator.Validate(ValidationContext context) { + ValidationResult IValidator.Validate(IValidationContext context) { context.Guard("Cannot pass null to Validate", nameof(context)); return Validate(ValidationContext.GetFromNonGenericContext(context)); } - Task IValidator.ValidateAsync(ValidationContext context, CancellationToken cancellation) { + Task IValidator.ValidateAsync(IValidationContext context, CancellationToken cancellation) { context.Guard("Cannot pass null to Validate", nameof(context)); return ValidateAsync(ValidationContext.GetFromNonGenericContext(context), cancellation); } @@ -189,14 +181,14 @@ public IRuleBuilderInitial RuleFor(Expression /// Invokes a rule for each item in the collection /// - /// Type of property + /// Type of property /// Expression representing the collection to validate /// An IRuleBuilder instance on which validators can be defined - public IRuleBuilderInitialCollection RuleForEach(Expression>> expression) { + public IRuleBuilderInitialCollection RuleForEach(Expression>> expression) { expression.Guard("Cannot pass null to RuleForEach", nameof(expression)); - var rule = CollectionPropertyRule.Create(expression, () => CascadeMode); + var rule = CollectionPropertyRule.Create(expression, () => CascadeMode); AddRule(rule); - var ruleBuilder = new RuleBuilder(rule, this); + var ruleBuilder = new RuleBuilder(rule, this); return ruleBuilder; } @@ -299,7 +291,7 @@ public IConditionBuilder UnlessAsync(Func, CancellationT /// public void Include(IValidator rulesToInclude) { rulesToInclude.Guard("Cannot pass null to Include", nameof(rulesToInclude)); - var rule = IncludeRule.Create(rulesToInclude, () => CascadeMode); + var rule = IncludeRule.Create(rulesToInclude, () => CascadeMode); AddRule(rule); } @@ -308,7 +300,7 @@ public void Include(IValidator rulesToInclude) { /// public void Include(Func rulesToInclude) where TValidator : IValidator { rulesToInclude.Guard("Cannot pass null to Include", nameof(rulesToInclude)); - var rule = IncludeRule.Create(rulesToInclude, () => CascadeMode); + var rule = IncludeRule.Create(rulesToInclude, () => CascadeMode); AddRule(rule); } diff --git a/src/FluentValidation/DefaultValidatorExtensions.cs b/src/FluentValidation/DefaultValidatorExtensions.cs index 8ab1c406b..8c3716038 100644 --- a/src/FluentValidation/DefaultValidatorExtensions.cs +++ b/src/FluentValidation/DefaultValidatorExtensions.cs @@ -22,6 +22,7 @@ namespace FluentValidation { using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; + using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -278,8 +279,11 @@ public static IRuleBuilderOptions NotEqual(this IRul if (comparer == null && typeof(TProperty) == typeof(string)) { comparer = StringComparer.Ordinal; } - var func = expression.Compile(); - return ruleBuilder.SetValidator(new NotEqualValidator(func.CoerceToNonGeneric(), expression.GetMember(), comparer)); + + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + string comparisonPropertyName = GetDisplayName(member, expression); + return ruleBuilder.SetValidator(new NotEqualValidator(func.CoerceToNonGeneric(), member, comparisonPropertyName, comparer)); } /// @@ -315,8 +319,11 @@ public static IRuleBuilderOptions Equal(this IRuleBu if (comparer == null && typeof(TProperty) == typeof(string)) { comparer = StringComparer.Ordinal; } - var func = expression.Compile(); - return ruleBuilder.SetValidator(new EqualValidator(func.CoerceToNonGeneric(), expression.GetMember(), comparer)); + + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); + return ruleBuilder.SetValidator(new EqualValidator(func.CoerceToNonGeneric(), member, name, comparer)); } /// @@ -331,7 +338,6 @@ public static IRuleBuilderOptions Equal(this IRuleBu /// public static IRuleBuilderOptions Must(this IRuleBuilder ruleBuilder, Func predicate) { predicate.Guard("Cannot pass a null predicate to Must.", nameof(predicate)); - return ruleBuilder.Must((x, val) => predicate(val)); } @@ -553,9 +559,10 @@ public static IRuleBuilderOptions LessThan(this IRul where TProperty : IComparable, IComparable { expression.Guard("Cannot pass null to LessThan", nameof(expression)); - var func = expression.Compile(); - - return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); + return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -573,9 +580,11 @@ public static IRuleBuilderOptions LessThan(this IRul where TProperty : struct, IComparable, IComparable { expression.Guard("Cannot pass null to LessThan", nameof(expression)); - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -593,9 +602,11 @@ public static IRuleBuilderOptions> LessThan where TProperty : struct, IComparable, IComparable { expression.Guard("Cannot pass null to LessThan", nameof(expression)); - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -613,9 +624,11 @@ public static IRuleBuilderOptions> LessThan where TProperty : struct, IComparable, IComparable { expression.Guard("Cannot pass null to LessThan", nameof(expression)); - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -631,9 +644,11 @@ public static IRuleBuilderOptions> LessThan public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -649,9 +664,11 @@ public static IRuleBuilderOptions LessThanOrEqualTo( public static IRuleBuilderOptions LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression>> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -667,9 +684,11 @@ public static IRuleBuilderOptions LessThanOrEqualTo( public static IRuleBuilderOptions> LessThanOrEqualTo( this IRuleBuilder> ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -685,9 +704,11 @@ public static IRuleBuilderOptions> LessThanOrEqualTo LessThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new LessThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -703,9 +724,11 @@ public static IRuleBuilderOptions> LessThanOrEqualTo GreaterThan(this IRuleBuilder ruleBuilder, Expression> expression) where TProperty : IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -721,9 +744,11 @@ public static IRuleBuilderOptions GreaterThan(this I public static IRuleBuilderOptions GreaterThan(this IRuleBuilder ruleBuilder, Expression>> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -739,9 +764,11 @@ public static IRuleBuilderOptions GreaterThan(this I public static IRuleBuilderOptions> GreaterThan(this IRuleBuilder> ruleBuilder, Expression> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -757,9 +784,11 @@ public static IRuleBuilderOptions> GreaterThan> GreaterThan(this IRuleBuilder> ruleBuilder, Expression>> expression) where TProperty : struct, IComparable, IComparable { - var func = expression.Compile(); + var member = expression.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, expression); + var name = GetDisplayName(member, expression); - return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), expression.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -775,9 +804,11 @@ public static IRuleBuilderOptions> GreaterThan GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : IComparable, IComparable { - var func = valueToCompare.Compile(); + var member = valueToCompare.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, valueToCompare); + var name = GetDisplayName(member, valueToCompare); - return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), valueToCompare.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -793,9 +824,11 @@ public static IRuleBuilderOptions GreaterThanOrEqualTo GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression>> valueToCompare) where TProperty : struct, IComparable, IComparable { - var func = valueToCompare.Compile(); + var member = valueToCompare.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, valueToCompare); + var name = GetDisplayName(member, valueToCompare); - return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), valueToCompare.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -810,8 +843,11 @@ public static IRuleBuilderOptions GreaterThanOrEqualTo public static IRuleBuilderOptions GreaterThanOrEqualTo(this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : struct, IComparable, IComparable { - var func = valueToCompare.Compile(); - return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), valueToCompare.GetMember())); + var member = valueToCompare.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, valueToCompare); + var name = GetDisplayName(member, valueToCompare); + + return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -827,9 +863,11 @@ public static IRuleBuilderOptions GreaterThanOrEqualTo GreaterThanOrEqualTo( this IRuleBuilder ruleBuilder, Expression> valueToCompare) where TProperty : struct, IComparable, IComparable { - var func = valueToCompare.Compile(); + var member = valueToCompare.GetMember(); + var func = AccessorCache.GetCachedAccessor(member, valueToCompare); + var name = GetDisplayName(member, valueToCompare); - return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), valueToCompare.GetMember())); + return ruleBuilder.SetValidator(new GreaterThanOrEqualValidator(func.CoerceToNonGeneric(), member, name)); } /// @@ -1153,5 +1191,9 @@ public static IRuleBuilderOptions ChildRules(this IR action(validator); return ruleBuilder.SetValidator(validator); } + + private static string GetDisplayName(MemberInfo member, Expression> expression) { + return ValidatorOptions.Global.DisplayNameResolver(typeof(T), member, expression) ?? member?.Name.SplitPascalCase(); + } } } diff --git a/src/FluentValidation/DefaultValidatorOptions.cs b/src/FluentValidation/DefaultValidatorOptions.cs index d7cacf0e6..ad257b0a7 100644 --- a/src/FluentValidation/DefaultValidatorOptions.cs +++ b/src/FluentValidation/DefaultValidatorOptions.cs @@ -55,21 +55,6 @@ public static IRuleBuilderInitialCollection Cascade( }); } - /// - /// Transforms the property value before validation occurs. The transformed value must be of the same type as the input value. - /// - /// - /// - /// - /// - /// - public static IRuleBuilderInitialCollection Transform(this IRuleBuilderInitialCollection ruleBuilder, Func transformationFunc) { - return ruleBuilder.Configure(cfg => { - cfg.Transformer = transformationFunc.CoerceToNonGeneric(); - }); - } - - /// /// Specifies a custom action to be invoked when the validator fails. /// diff --git a/src/FluentValidation/FluentValidation.csproj b/src/FluentValidation/FluentValidation.csproj index f163b0129..4f419d475 100644 --- a/src/FluentValidation/FluentValidation.csproj +++ b/src/FluentValidation/FluentValidation.csproj @@ -32,6 +32,8 @@ Changes in 9.0.0: * Additional ValidationException constructor that allows using both the default message and a custom one together. * ScalePrecisionValidator algorithm now matches SQL Server. * Additional overload of the When methods that contain the validation context. +* Automatically scanned types can be excluded when calling RegisterValidatorsFromAssemblyContaining in ASP.NET Core projects. +* New AutomaticValidationEnabled property for use in ASP.NET Core projects (defaults to true). Full release notes can be found at https://github.com/FluentValidation/FluentValidation/blob/master/Changelog.txt diff --git a/src/FluentValidation/ValidationContext.cs b/src/FluentValidation/IValidationContext.cs similarity index 51% rename from src/FluentValidation/ValidationContext.cs rename to src/FluentValidation/IValidationContext.cs index 2b8becb71..c86b53fe2 100644 --- a/src/FluentValidation/ValidationContext.cs +++ b/src/FluentValidation/IValidationContext.cs @@ -1,21 +1,3 @@ -#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 { using System; using System.Collections.Generic; @@ -24,7 +6,7 @@ namespace FluentValidation { /// /// Defines a validation context. /// - public interface IValidationContext { + public interface ICommonContext { /// /// The object currently being validated. /// @@ -38,115 +20,88 @@ public interface IValidationContext { /// /// Parent validation context. /// - IValidationContext ParentContext { get; } + ICommonContext ParentContext { get; } } - /// - /// Validation context - /// - /// - public class ValidationContext : ValidationContext { + public interface IValidationContext : ICommonContext { /// - /// Creates a new validation context + /// Additional data associated with the validation request. /// - /// - public ValidationContext(T instanceToValidate) : this(instanceToValidate, new PropertyChain(), ValidatorOptions.ValidatorSelectors.DefaultValidatorSelectorFactory()) { - - } + IDictionary RootContextData { get; } /// - /// Creates a new validation context with a custom property chain and selector + /// Property chain /// - /// - /// - /// - public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IValidatorSelector validatorSelector) - : base(instanceToValidate, propertyChain, validatorSelector) { - - InstanceToValidate = instanceToValidate; - } + PropertyChain PropertyChain { get; } /// - /// The object to validate + /// Selector /// - public new T InstanceToValidate { get; private set; } + IValidatorSelector Selector { get; } /// - /// Gets or creates generic validation context from non-generic validation context. + /// Whether this is a child context /// - /// - /// - /// - /// - public static ValidationContext GetFromNonGenericContext(ValidationContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + bool IsChildContext { get; } - // Already of the correct type. - if (context is ValidationContext c) { - return c; - } - - // Parameters match - if (context.InstanceToValidate is T) { - return context.ToGeneric(); - } - - if (context.InstanceToValidate == null) { - return new ValidationContext(default, context.PropertyChain, context.Selector) { - IsChildContext = context.IsChildContext, - RootContextData = context.RootContextData, - _parentContext = ((IValidationContext)context).ParentContext - }; - } - - throw new InvalidOperationException($"Cannot validate instances of type '{context.InstanceToValidate.GetType().Name}'. This validator can only validate instances of type '{typeof(T).Name}'."); - } + /// + /// Whether this is a child collection context. + /// + bool IsChildCollectionContext { get; } } /// /// Validation context /// - public class ValidationContext : IValidationContext { - private protected IValidationContext _parentContext; - - /// - /// Additional data associated with the validation request. - /// - public IDictionary RootContextData { get; private protected set; } = new Dictionary(); + /// + public class ValidationContext : IValidationContext { + private ICommonContext _parentContext; /// /// Creates a new validation context /// /// - public ValidationContext(object instanceToValidate) - : this (instanceToValidate, new PropertyChain(), ValidatorOptions.ValidatorSelectors.DefaultValidatorSelectorFactory()){ - + public ValidationContext(T instanceToValidate) : this(instanceToValidate, new PropertyChain(), ValidatorOptions.ValidatorSelectors.DefaultValidatorSelectorFactory()) { } /// - /// Creates a new validation context with a property chain and validation selector + /// Creates a new validation context with a custom property chain and selector /// /// /// /// - public ValidationContext(object instanceToValidate, PropertyChain propertyChain, IValidatorSelector validatorSelector) { + public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IValidatorSelector validatorSelector) { PropertyChain = new PropertyChain(propertyChain); InstanceToValidate = instanceToValidate; Selector = validatorSelector; } + /// + /// The object to validate + /// + public T InstanceToValidate { get; private set; } + + /// + /// Additional data associated with the validation request. + /// + public IDictionary RootContextData { get; private protected set; } = new Dictionary(); + + /// /// Property chain /// public PropertyChain PropertyChain { get; private set; } + /// /// Object being validated /// - public object InstanceToValidate { get; private set; } + object ICommonContext.InstanceToValidate => InstanceToValidate; + /// /// Selector /// public IValidatorSelector Selector { get; private set; } + /// /// Whether this is a child context /// @@ -157,26 +112,47 @@ public ValidationContext(object instanceToValidate, PropertyChain propertyChain, /// public virtual bool IsChildCollectionContext { get; internal set; } - // root level context doesn't know about properties. - object IValidationContext.PropertyValue => null; + object ICommonContext.PropertyValue => null; // This is the root context so it doesn't have a parent. // Explicit implementation so it's not exposed necessarily. - IValidationContext IValidationContext.ParentContext => _parentContext; + ICommonContext ICommonContext.ParentContext => _parentContext; + /// - /// Creates a new ValidationContext based on this one + /// Gets or creates generic validation context from non-generic validation context. /// - /// - /// - /// + /// /// - public ValidationContext Clone(PropertyChain chain = null, object instanceToValidate = null, IValidatorSelector selector = null) { - return new ValidationContext(instanceToValidate ?? this.InstanceToValidate, chain ?? this.PropertyChain, selector ?? this.Selector) { - RootContextData = RootContextData, - _parentContext = this, - }; + /// + /// + public static ValidationContext GetFromNonGenericContext(IValidationContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + + // Already of the correct type. + if (context is ValidationContext c) { + return c; + } + + // Parameters match + if (context.InstanceToValidate is T instanceToValidate) { + return new ValidationContext(instanceToValidate, context.PropertyChain, context.Selector) { + IsChildContext = context.IsChildContext, + RootContextData = context.RootContextData, + _parentContext = context.ParentContext + }; + } + + if (context.InstanceToValidate == null) { + return new ValidationContext(default, context.PropertyChain, context.Selector) { + IsChildContext = context.IsChildContext, + RootContextData = context.RootContextData, + _parentContext = context.ParentContext + }; + } + + throw new InvalidOperationException($"Cannot validate instances of type '{context.InstanceToValidate.GetType().Name}'. This validator can only validate instances of type '{typeof(T).Name}'."); } /// @@ -186,8 +162,8 @@ public ValidationContext Clone(PropertyChain chain = null, object instanceToVali /// /// /// - public ValidationContext CloneForChildValidator(object instanceToValidate, bool preserveParentContext = false, IValidatorSelector selector = null) { - return new ValidationContext(instanceToValidate, PropertyChain, selector ?? Selector) { + public ValidationContext CloneForChildValidator(TChild instanceToValidate, bool preserveParentContext = false, IValidatorSelector selector = null) { + return new ValidationContext(instanceToValidate, PropertyChain, selector ?? Selector) { IsChildContext = true, RootContextData = RootContextData, _parentContext = preserveParentContext ? this : null @@ -200,28 +176,13 @@ public ValidationContext CloneForChildValidator(object instanceToValidate, bool /// /// /// - public ValidationContext CloneForChildCollectionValidator(object instanceToValidate, bool preserveParentContext = false) { - return new ValidationContext(instanceToValidate, null, Selector) { + public ValidationContext CloneForChildCollectionValidator(TNew instanceToValidate, bool preserveParentContext = false) { + return new ValidationContext(instanceToValidate, null, Selector) { IsChildContext = true, IsChildCollectionContext = true, RootContextData = RootContextData, _parentContext = preserveParentContext ? this : null }; } - - /// - /// Converts a non-generic ValidationContext to a generic version. - /// No type check is performed. - /// - /// - /// - internal ValidationContext ToGeneric() { - return new ValidationContext((T)InstanceToValidate, PropertyChain, Selector) { - IsChildContext = IsChildContext, - RootContextData = RootContextData, - _parentContext = _parentContext - }; - } - } } diff --git a/src/FluentValidation/IValidationRule.cs b/src/FluentValidation/IValidationRule.cs index 72f7e56e3..c40967803 100644 --- a/src/FluentValidation/IValidationRule.cs +++ b/src/FluentValidation/IValidationRule.cs @@ -43,7 +43,7 @@ public interface IValidationRule { /// /// Validation Context /// A collection of validation failures - IEnumerable Validate(ValidationContext context); + IEnumerable Validate(IValidationContext context); /// /// Performs validation using a validation context and returns a collection of Validation Failures asynchronously. @@ -51,7 +51,7 @@ public interface IValidationRule { /// Validation Context /// Cancellation token /// A collection of validation failures - Task> ValidateAsync(ValidationContext context, CancellationToken cancellation); + Task> ValidateAsync(IValidationContext context, CancellationToken cancellation); /// /// Applies a condition to either all the validators in the rule, or the most recent validator in the rule chain. @@ -71,12 +71,12 @@ public interface IValidationRule { /// Applies a condition that wraps the entire rule. /// /// The condition to apply. - void ApplySharedCondition(Func condition); + void ApplySharedCondition(Func condition); /// /// Applies an asynchronous condition that wraps the entire rule. /// /// The condition to apply. - void ApplySharedAsyncCondition(Func> condition); + void ApplySharedAsyncCondition(Func> condition); } } diff --git a/src/FluentValidation/IValidator.cs b/src/FluentValidation/IValidator.cs index b2372f361..1c4b811ca 100644 --- a/src/FluentValidation/IValidator.cs +++ b/src/FluentValidation/IValidator.cs @@ -55,27 +55,12 @@ public interface IValidator : IValidator { /// Defines a validator for a particular type. /// public interface IValidator { - /// - /// Validates the specified instance - /// - /// - /// A ValidationResult containing any validation failures - ValidationResult Validate(object instance); - - /// - /// Validates the specified instance asynchronously - /// - /// - /// Cancellation token - /// A ValidationResult containing any validation failures - Task ValidateAsync(object instance, CancellationToken cancellation = new CancellationToken()); - /// /// Validates the specified instance. /// /// A ValidationContext /// A ValidationResult object contains any validation failures. - ValidationResult Validate(ValidationContext context); + ValidationResult Validate(IValidationContext context); /// /// Validates the specified instance asynchronously. @@ -83,7 +68,7 @@ public interface IValidator { /// A ValidationContext /// Cancellation token /// A ValidationResult object contains any validation failures. - Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()); + Task ValidateAsync(IValidationContext context, CancellationToken cancellation = new CancellationToken()); /// /// Creates a hook to access various meta data properties diff --git a/src/FluentValidation/Internal/CollectionPropertyRule.cs b/src/FluentValidation/Internal/CollectionPropertyRule.cs index 3987ea8c2..9fdb0d574 100644 --- a/src/FluentValidation/Internal/CollectionPropertyRule.cs +++ b/src/FluentValidation/Internal/CollectionPropertyRule.cs @@ -32,8 +32,9 @@ namespace FluentValidation.Internal { /// /// Rule definition for collection properties /// - /// - public class CollectionPropertyRule : PropertyRule { + /// + /// + public class CollectionPropertyRule : PropertyRule { /// /// Initializes new instance of the CollectionPropertyRule class /// @@ -49,22 +50,22 @@ public CollectionPropertyRule(MemberInfo member, Func propertyFu /// /// Filter that should include/exclude items in the collection. /// - public Func Filter { get; set; } + public Func Filter { get; set; } /// /// Constructs the indexer in the property name associated with the error message. /// By default this is "[" + index + "]" /// - public Func, TProperty, int, string> IndexBuilder { get; set; } + public Func, TElement, int, string> IndexBuilder { get; set; } /// /// Creates a new property rule from a lambda expression. /// - public static CollectionPropertyRule Create(Expression>> expression, Func cascadeModeThunk) { + public static CollectionPropertyRule Create(Expression>> expression, Func cascadeModeThunk) { var member = expression.GetMember(); var compiled = expression.Compile(); - return new CollectionPropertyRule(member, compiled.CoerceToNonGeneric(), expression, cascadeModeThunk, typeof(TProperty), typeof(T)); + return new CollectionPropertyRule(member, compiled.CoerceToNonGeneric(), expression, cascadeModeThunk, typeof(TElement), typeof(T)); } /// @@ -75,7 +76,7 @@ public static CollectionPropertyRule Create(Expression /// /// - protected override async Task> InvokePropertyValidatorAsync(ValidationContext context, IPropertyValidator validator, string propertyName, CancellationToken cancellation) { + protected override async Task> InvokePropertyValidatorAsync(IValidationContext context, IPropertyValidator validator, string propertyName, CancellationToken cancellation) { if (string.IsNullOrEmpty(propertyName)) { propertyName = InferPropertyName(Expression); } @@ -85,13 +86,15 @@ protected override async Task> InvokePropertyVali if (validator.Options.Condition != null && !validator.Options.Condition(propertyContext)) return Enumerable.Empty(); if (validator.Options.AsyncCondition != null && !await validator.Options.AsyncCondition(propertyContext, cancellation)) return Enumerable.Empty(); - var collectionPropertyValue = propertyContext.PropertyValue as IEnumerable; + var collectionPropertyValue = propertyContext.PropertyValue as IEnumerable; if (collectionPropertyValue != null) { if (string.IsNullOrEmpty(propertyName)) { throw new InvalidOperationException("Could not automatically determine the property name "); } + var actualContext = ValidationContext.GetFromNonGenericContext(context); + var validatorTasks = collectionPropertyValue.Select(async (v, index) => { if (Filter != null && !Filter(v)) { return Enumerable.Empty(); @@ -105,7 +108,7 @@ protected override async Task> InvokePropertyVali useDefaultIndexFormat = false; } - var newContext = context.CloneForChildCollectionValidator(context.InstanceToValidate, preserveParentContext: true); + ValidationContext newContext = actualContext.CloneForChildCollectionValidator(actualContext.InstanceToValidate, preserveParentContext: true); newContext.PropertyChain.Add(propertyName); newContext.PropertyChain.AddIndexer(indexer, useDefaultIndexFormat); @@ -145,7 +148,7 @@ private string InferPropertyName(LambdaExpression expression) { /// /// /// - protected override IEnumerable InvokePropertyValidator(ValidationContext context, Validators.IPropertyValidator validator, string propertyName) { + protected override IEnumerable InvokePropertyValidator(IValidationContext context, Validators.IPropertyValidator validator, string propertyName) { if (string.IsNullOrEmpty(propertyName)) { propertyName = InferPropertyName(Expression); } @@ -155,7 +158,7 @@ private string InferPropertyName(LambdaExpression expression) { if (validator.Options.Condition != null && !validator.Options.Condition(propertyContext)) return Enumerable.Empty(); var results = new List(); - var collectionPropertyValue = propertyContext.PropertyValue as IEnumerable; + var collectionPropertyValue = propertyContext.PropertyValue as IEnumerable; int count = 0; @@ -164,6 +167,8 @@ private string InferPropertyName(LambdaExpression expression) { throw new InvalidOperationException("Could not automatically determine the property name "); } + var actualContext = ValidationContext.GetFromNonGenericContext(context); + foreach (var element in collectionPropertyValue) { int index = count++; @@ -179,7 +184,7 @@ private string InferPropertyName(LambdaExpression expression) { useDefaultIndexFormat = false; } - var newContext = context.CloneForChildCollectionValidator(context.InstanceToValidate, preserveParentContext: true); + ValidationContext newContext = actualContext.CloneForChildCollectionValidator(actualContext.InstanceToValidate, preserveParentContext: true); newContext.PropertyChain.Add(propertyName); newContext.PropertyChain.AddIndexer(indexer, useDefaultIndexFormat); diff --git a/src/FluentValidation/Internal/ConditionBuilder.cs b/src/FluentValidation/Internal/ConditionBuilder.cs index 41b7f1297..0207080c9 100644 --- a/src/FluentValidation/Internal/ConditionBuilder.cs +++ b/src/FluentValidation/Internal/ConditionBuilder.cs @@ -46,7 +46,7 @@ public IConditionBuilder When(Func, bool> predicate, Act // Generate unique ID for this shared condition. var id = "_FV_Condition_" + Guid.NewGuid(); - bool Condition(ValidationContext context) { + bool Condition(IValidationContext context) { string cacheId = null; if (context.InstanceToValidate != null) { @@ -107,7 +107,7 @@ public IConditionBuilder WhenAsync(Func, CancellationTok // Generate unique ID for this shared condition. var id = "_FV_AsyncCondition_" + Guid.NewGuid(); - async Task Condition(ValidationContext context, CancellationToken ct) { + async Task Condition(IValidationContext context, CancellationToken ct) { string cacheId = null; if (context.InstanceToValidate != null) { cacheId = id + context.InstanceToValidate.GetHashCode(); @@ -145,9 +145,9 @@ public IConditionBuilder UnlessAsync(Func, CancellationT internal class ConditionOtherwiseBuilder : IConditionBuilder { private TrackingCollection _rules; - private readonly Func _condition; + private readonly Func _condition; - public ConditionOtherwiseBuilder(TrackingCollection rules, Func condition) { + public ConditionOtherwiseBuilder(TrackingCollection rules, Func condition) { _rules = rules; _condition = condition; } @@ -169,9 +169,9 @@ public virtual void Otherwise(Action action) { internal class AsyncConditionOtherwiseBuilder : IConditionBuilder { private TrackingCollection _rules; - private readonly Func> _condition; + private readonly Func> _condition; - public AsyncConditionOtherwiseBuilder(TrackingCollection rules, Func> condition) { + public AsyncConditionOtherwiseBuilder(TrackingCollection rules, Func> condition) { _rules = rules; _condition = condition; } diff --git a/src/FluentValidation/Internal/DefaultValidatorSelector.cs b/src/FluentValidation/Internal/DefaultValidatorSelector.cs index 1dd3092c5..a6d954418 100644 --- a/src/FluentValidation/Internal/DefaultValidatorSelector.cs +++ b/src/FluentValidation/Internal/DefaultValidatorSelector.cs @@ -31,7 +31,7 @@ public class DefaultValidatorSelector : IValidatorSelector { /// Property path (eg Customer.Address.Line1) /// Contextual information /// Whether or not the validator can execute. - public bool CanExecute(IValidationRule rule, string propertyPath, ValidationContext context) { + public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) { // By default we ignore any rules part of a RuleSet. if (rule.RuleSets.Length > 0 && !rule.RuleSets.Contains("default", StringComparer.OrdinalIgnoreCase)) { return false; diff --git a/src/FluentValidation/Internal/Extensions.cs b/src/FluentValidation/Internal/Extensions.cs index 2b17c2440..4848c4987 100644 --- a/src/FluentValidation/Internal/Extensions.cs +++ b/src/FluentValidation/Internal/Extensions.cs @@ -194,7 +194,7 @@ public static Action CoerceToNonGeneric(this Action action) { /// /// /// - public static bool IsAsync(this ValidationContext ctx) { + public static bool IsAsync(this IValidationContext ctx) { if (ctx.RootContextData.ContainsKey("__FV_IsAsyncExecution")) { return (ctx.RootContextData["__FV_IsAsyncExecution"] as bool?).GetValueOrDefault(); } diff --git a/src/FluentValidation/Internal/IValidatorSelector.cs b/src/FluentValidation/Internal/IValidatorSelector.cs index 01f9ab098..3413f4f8a 100644 --- a/src/FluentValidation/Internal/IValidatorSelector.cs +++ b/src/FluentValidation/Internal/IValidatorSelector.cs @@ -29,6 +29,6 @@ public interface IValidatorSelector { /// Property path (eg Customer.Address.Line1) /// Contextual information /// Whether or not the validator can execute. - bool CanExecute(IValidationRule rule, string propertyPath, ValidationContext context); + bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context); } } diff --git a/src/FluentValidation/Internal/IncludeRule.cs b/src/FluentValidation/Internal/IncludeRule.cs index 452f8d339..b191d3dc9 100644 --- a/src/FluentValidation/Internal/IncludeRule.cs +++ b/src/FluentValidation/Internal/IncludeRule.cs @@ -7,10 +7,15 @@ namespace FluentValidation.Internal { using Results; using Validators; + /// + /// Marker interface indicating an include rule. + /// + public interface IIncludeRule { } + /// /// Include rule /// - public class IncludeRule : PropertyRule { + public class IncludeRule : PropertyRule, IIncludeRule { /// /// Creates a new IncludeRule /// @@ -18,8 +23,8 @@ public class IncludeRule : PropertyRule { /// /// /// - public IncludeRule(IValidator validator, Func cascadeModeThunk, Type typeToValidate, Type containerType) : base(null, x => x, null, cascadeModeThunk, typeToValidate, containerType) { - AddValidator(new ChildValidatorAdaptor(validator, validator.GetType())); + public IncludeRule(IValidator validator, Func cascadeModeThunk, Type typeToValidate, Type containerType) : base(null, x => x, null, cascadeModeThunk, typeToValidate, containerType) { + AddValidator(new ChildValidatorAdaptor(validator, validator.GetType())); } /// @@ -30,10 +35,10 @@ public IncludeRule(IValidator validator, Func cascadeModeThunk, Typ /// /// /// - public IncludeRule(Func func, Func cascadeModeThunk, Type typeToValidate, Type containerType, Type validatorType) : base(null, x => x, null, cascadeModeThunk, typeToValidate, containerType) { - AddValidator(new ChildValidatorAdaptor(func, validatorType)); + public IncludeRule(Func> func, Func cascadeModeThunk, Type typeToValidate, Type containerType, Type validatorType) : base(null, x => x, null, cascadeModeThunk, typeToValidate, containerType) { + AddValidator(new ChildValidatorAdaptor(func, validatorType)); } - + /// /// Creates a new include rule from an existing validator /// @@ -41,8 +46,8 @@ public IncludeRule(Func func, Func /// /// /// - public static IncludeRule Create(IValidator validator, Func cascadeModeThunk) { - return new IncludeRule(validator, cascadeModeThunk, typeof(T), typeof(T)); + public static IncludeRule Create(IValidator validator, Func cascadeModeThunk) { + return new IncludeRule(validator, cascadeModeThunk, typeof(T), typeof(T)); } /// @@ -53,20 +58,20 @@ public static IncludeRule Create(IValidator validator, Func casc /// /// /// - public static IncludeRule Create(Func func, Func cascadeModeThunk) + public static IncludeRule Create(Func func, Func cascadeModeThunk) where TValidator : IValidator { - return new IncludeRule(ctx => func((T)ctx.InstanceToValidate), cascadeModeThunk, typeof(T), typeof(T), typeof(TValidator)); + return new IncludeRule(ctx => func((T)ctx.InstanceToValidate), cascadeModeThunk, typeof(T), typeof(T), typeof(TValidator)); } - public override IEnumerable Validate(ValidationContext context) { + public override IEnumerable Validate(IValidationContext context) { context.RootContextData[MemberNameValidatorSelector.DisableCascadeKey] = true; var result = base.Validate(context).ToList(); context.RootContextData.Remove(MemberNameValidatorSelector.DisableCascadeKey); return result; } - public override async Task> ValidateAsync(ValidationContext context, CancellationToken cancellation) { + public override async Task> ValidateAsync(IValidationContext context, CancellationToken cancellation) { context.RootContextData[MemberNameValidatorSelector.DisableCascadeKey] = true; var result = await base.ValidateAsync(context, cancellation); result = result.ToList(); @@ -74,4 +79,4 @@ public override async Task> ValidateAsync(Validat return result; } } -} \ No newline at end of file +} diff --git a/src/FluentValidation/Internal/MemberNameValidatorSelector.cs b/src/FluentValidation/Internal/MemberNameValidatorSelector.cs index 621455669..d610e7470 100644 --- a/src/FluentValidation/Internal/MemberNameValidatorSelector.cs +++ b/src/FluentValidation/Internal/MemberNameValidatorSelector.cs @@ -48,7 +48,7 @@ public MemberNameValidatorSelector(IEnumerable memberNames) { /// Property path (eg Customer.Address.Line1) /// Contextual information /// Whether or not the validator can execute. - public bool CanExecute (IValidationRule rule, string propertyPath, ValidationContext context) { + public bool CanExecute (IValidationRule rule, string propertyPath, IValidationContext context) { // Validator selector only applies to the top level. // If we're running in a child context then this means that the child validator has already been selected // Because of this, we assume that the rule should continue (ie if the parent rule is valid, all children are valid) @@ -56,7 +56,7 @@ public bool CanExecute (IValidationRule rule, string propertyPath, ValidationCon bool cascadeEnabled = !context.RootContextData.ContainsKey(DisableCascadeKey); return (isChildContext && cascadeEnabled && !_memberNames.Any(x => x.Contains("."))) - || rule is IncludeRule + || rule is IIncludeRule || ( _memberNames.Any(x => x == propertyPath || propertyPath.StartsWith(x + ".") || x.StartsWith(propertyPath + "."))); } diff --git a/src/FluentValidation/Internal/MessageBuilderContext.cs b/src/FluentValidation/Internal/MessageBuilderContext.cs index 851bdbf30..208555037 100644 --- a/src/FluentValidation/Internal/MessageBuilderContext.cs +++ b/src/FluentValidation/Internal/MessageBuilderContext.cs @@ -2,7 +2,7 @@ using Resources; using Validators; - public class MessageBuilderContext : IValidationContext { + public class MessageBuilderContext : ICommonContext { private PropertyValidatorContext _innerContext; public MessageBuilderContext(PropertyValidatorContext innerContext, IStringSource errorSource, IPropertyValidator propertyValidator) { @@ -15,7 +15,7 @@ public MessageBuilderContext(PropertyValidatorContext innerContext, IStringSourc public IStringSource ErrorSource { get; } - public ValidationContext ParentContext => _innerContext.ParentContext; + public IValidationContext ParentContext => _innerContext.ParentContext; public PropertyRule Rule => _innerContext.Rule; @@ -28,7 +28,7 @@ public MessageBuilderContext(PropertyValidatorContext innerContext, IStringSourc public object InstanceToValidate => _innerContext.InstanceToValidate; public object PropertyValue => _innerContext.PropertyValue; - IValidationContext IValidationContext.ParentContext => ParentContext; + ICommonContext ICommonContext.ParentContext => ParentContext; public string GetDefaultMessage() { return MessageFormatter.BuildMessage(ErrorSource.GetString(_innerContext)); diff --git a/src/FluentValidation/Internal/PropertyRule.cs b/src/FluentValidation/Internal/PropertyRule.cs index 7018aa7b8..7de3497fe 100644 --- a/src/FluentValidation/Internal/PropertyRule.cs +++ b/src/FluentValidation/Internal/PropertyRule.cs @@ -37,18 +37,18 @@ public class PropertyRule : IValidationRule { string _propertyDisplayName; string _propertyName; private string[] _ruleSet = new string[0]; - private Func _condition; - private Func> _asyncCondition; + private Func _condition; + private Func> _asyncCondition; /// /// Condition for all validators in this rule. /// - public Func Condition => _condition; + public Func Condition => _condition; /// /// Asynchronous condition for all validators in this rule. /// - public Func> AsyncCondition => _asyncCondition; + public Func> AsyncCondition => _asyncCondition; /// /// Property associated with this rule. @@ -219,7 +219,7 @@ public string GetDisplayName() { /// /// Display name for the property. /// - public string GetDisplayName(IValidationContext context) { + public string GetDisplayName(ICommonContext context) { string result = null; if (DisplayName != null) { @@ -238,7 +238,7 @@ public string GetDisplayName(IValidationContext context) { /// /// Validation Context /// A collection of validation failures - public virtual IEnumerable Validate(ValidationContext context) { + public virtual IEnumerable Validate(IValidationContext context) { string displayName = GetDisplayName(context); if (PropertyName == null && displayName == null) { @@ -314,7 +314,7 @@ public virtual IEnumerable Validate(ValidationContext context /// Validation Context /// /// A collection of validation failures - public virtual async Task> ValidateAsync(ValidationContext context, CancellationToken cancellation) { + public virtual async Task> ValidateAsync(IValidationContext context, CancellationToken cancellation) { if (!context.IsAsync()) { context.RootContextData["__FV_IsAsyncExecution"] = true; } @@ -385,7 +385,7 @@ public virtual async Task> ValidateAsync(Validati return failures; } - private async Task> RunDependentRulesAsync(ValidationContext context, CancellationToken cancellation) { + private async Task> RunDependentRulesAsync(IValidationContext context, CancellationToken cancellation) { var failures = new List(); foreach (var rule in DependentRules) { @@ -404,7 +404,7 @@ private async Task> RunDependentRulesAsync(Valida /// /// /// - protected virtual async Task> InvokePropertyValidatorAsync(ValidationContext context, IPropertyValidator validator, string propertyName, CancellationToken cancellation) { + protected virtual async Task> InvokePropertyValidatorAsync(IValidationContext context, IPropertyValidator validator, string propertyName, CancellationToken cancellation) { var propertyContext = new PropertyValidatorContext(context, this, propertyName); if (validator.Options.Condition != null && !validator.Options.Condition(propertyContext)) return Enumerable.Empty(); if (validator.Options.AsyncCondition != null && !await validator.Options.AsyncCondition(propertyContext, cancellation)) return Enumerable.Empty(); @@ -414,7 +414,7 @@ protected virtual async Task> InvokePropertyValid /// /// Invokes a property validator using the specified validation context. /// - protected virtual IEnumerable InvokePropertyValidator(ValidationContext context, IPropertyValidator validator, string propertyName) { + protected virtual IEnumerable InvokePropertyValidator(IValidationContext context, IPropertyValidator validator, string propertyName) { var propertyContext = new PropertyValidatorContext(context, this, propertyName); if (validator.Options.Condition != null && !validator.Options.Condition(propertyContext)) return Enumerable.Empty(); return validator.Validate(propertyContext); @@ -462,7 +462,7 @@ public void ApplyAsyncCondition(Func condition) { + public void ApplySharedCondition(Func condition) { if (_condition == null) { _condition = condition; } @@ -472,7 +472,7 @@ public void ApplySharedCondition(Func condition) { } } - public void ApplySharedAsyncCondition(Func> condition) { + public void ApplySharedAsyncCondition(Func> condition) { if (_asyncCondition == null) { _asyncCondition = condition; } diff --git a/src/FluentValidation/Internal/RuleBuilder.cs b/src/FluentValidation/Internal/RuleBuilder.cs index c3951c029..9a6c1057f 100644 --- a/src/FluentValidation/Internal/RuleBuilder.cs +++ b/src/FluentValidation/Internal/RuleBuilder.cs @@ -63,7 +63,7 @@ public IRuleBuilderOptions SetValidator(IPropertyValidator validat /// public IRuleBuilderOptions SetValidator(IValidator validator, params string[] ruleSets) { validator.Guard("Cannot pass a null validator to SetValidator", nameof(validator)); - var adaptor = new ChildValidatorAdaptor(validator, validator.GetType()) { + var adaptor = new ChildValidatorAdaptor(validator, validator.GetType()) { RuleSets = ruleSets }; SetValidator(adaptor); @@ -78,7 +78,7 @@ public IRuleBuilderOptions SetValidator(IValidator vali public IRuleBuilderOptions SetValidator(Func validatorProvider, params string[] ruleSets) where TValidator : IValidator { validatorProvider.Guard("Cannot pass a null validatorProvider to SetValidator", nameof(validatorProvider)); - SetValidator(new ChildValidatorAdaptor(context => validatorProvider((T) context.InstanceToValidate), typeof (TValidator)) { + SetValidator(new ChildValidatorAdaptor(context => validatorProvider((T) context.InstanceToValidate), typeof (TValidator)) { RuleSets = ruleSets }); return this; @@ -91,7 +91,7 @@ public IRuleBuilderOptions SetValidator(Func public IRuleBuilderOptions SetValidator(Func validatorProvider, params string[] ruleSets) where TValidator : IValidator { validatorProvider.Guard("Cannot pass a null validatorProvider to SetValidator", nameof(validatorProvider)); - SetValidator(new ChildValidatorAdaptor(context => validatorProvider((T) context.InstanceToValidate, (TProperty) context.PropertyValue), typeof (TValidator)) { + SetValidator(new ChildValidatorAdaptor(context => validatorProvider((T) context.InstanceToValidate, (TProperty) context.PropertyValue), typeof (TValidator)) { RuleSets = ruleSets }); return this; @@ -101,9 +101,9 @@ public IRuleBuilderOptions SetValidator(Func /// The validator provider to set - public IRuleBuilderOptions SetValidator(Func validatorProvider) where TValidator : IValidator { + public IRuleBuilderOptions SetValidator(Func validatorProvider) where TValidator : IValidator { validatorProvider.Guard("Cannot pass a null validatorProvider to SetValidator", nameof(validatorProvider)); - SetValidator(new ChildValidatorAdaptor(context => validatorProvider(context), typeof (TValidator))); + SetValidator(new ChildValidatorAdaptor(context => validatorProvider(context), typeof (TValidator))); return this; } @@ -117,8 +117,8 @@ IRuleBuilderInitial IConfigurable IConfigurable, IRuleBuilderInitialCollection>.Configure(Action> configurator) { - configurator((CollectionPropertyRule) Rule); + IRuleBuilderInitialCollection IConfigurable, IRuleBuilderInitialCollection>.Configure(Action> configurator) { + configurator((CollectionPropertyRule) Rule); return this; } diff --git a/src/FluentValidation/Internal/RulesetValidatorSelector.cs b/src/FluentValidation/Internal/RulesetValidatorSelector.cs index c9655bed6..b02d369cf 100644 --- a/src/FluentValidation/Internal/RulesetValidatorSelector.cs +++ b/src/FluentValidation/Internal/RulesetValidatorSelector.cs @@ -30,9 +30,9 @@ public RulesetValidatorSelector(params string[] rulesetsToExecute) { /// Property path (eg Customer.Address.Line1) /// Contextual information /// Whether or not the validator can execute. - public virtual bool CanExecute(IValidationRule rule, string propertyPath, ValidationContext context) { + public virtual bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) { var executed = context.RootContextData.GetOrAdd("_FV_RuleSetsExecuted", () => new HashSet()); - + if (rule.RuleSets.Length == 0 && _rulesetsToExecute.Length > 0) { if (IsIncludeRule(rule)) { return true; @@ -78,7 +78,7 @@ public virtual bool CanExecute(IValidationRule rule, string propertyPath, Valida /// /// protected bool IsIncludeRule(IValidationRule rule) { - return rule is IncludeRule; + return rule is IIncludeRule; } } -} \ No newline at end of file +} diff --git a/src/FluentValidation/Resources/IStringSource.cs b/src/FluentValidation/Resources/IStringSource.cs index af57bead3..ba9f8a427 100644 --- a/src/FluentValidation/Resources/IStringSource.cs +++ b/src/FluentValidation/Resources/IStringSource.cs @@ -29,6 +29,6 @@ public interface IStringSource { /// Construct the error message template /// /// Error message template - string GetString(IValidationContext context); + string GetString(ICommonContext context); } } diff --git a/src/FluentValidation/Resources/LanguageStringSource.cs b/src/FluentValidation/Resources/LanguageStringSource.cs index 403fd4a47..de84551ce 100644 --- a/src/FluentValidation/Resources/LanguageStringSource.cs +++ b/src/FluentValidation/Resources/LanguageStringSource.cs @@ -26,18 +26,18 @@ namespace FluentValidation.Resources { /// public class LanguageStringSource : IStringSource { private readonly string _key; - internal Func ErrorCodeFunc { get; set; } + internal Func ErrorCodeFunc { get; set; } public LanguageStringSource(string key) { _key = key; } - public LanguageStringSource(Func errorCodeFunc, string fallbackKey) { + public LanguageStringSource(Func errorCodeFunc, string fallbackKey) { ErrorCodeFunc = errorCodeFunc; _key = fallbackKey; } - public virtual string GetString(IValidationContext context) { + public virtual string GetString(ICommonContext context) { var errorCode = ErrorCodeFunc?.Invoke(context); if (errorCode != null) { diff --git a/src/FluentValidation/Resources/LazyStringSource.cs b/src/FluentValidation/Resources/LazyStringSource.cs index ee3da5bc1..c3e3be51d 100644 --- a/src/FluentValidation/Resources/LazyStringSource.cs +++ b/src/FluentValidation/Resources/LazyStringSource.cs @@ -23,12 +23,12 @@ namespace FluentValidation.Resources { /// Lazily loads the string /// public class LazyStringSource : IStringSource { - readonly Func _stringProvider; + readonly Func _stringProvider; /// /// Creates a LazyStringSource /// - public LazyStringSource(Func stringProvider) { + public LazyStringSource(Func stringProvider) { _stringProvider = stringProvider; } @@ -36,7 +36,7 @@ public LazyStringSource(Func stringProvider) { /// Gets the value /// /// - public string GetString(IValidationContext context) { + public string GetString(ICommonContext context) { try { return _stringProvider(context); } diff --git a/src/FluentValidation/Resources/StaticStringSource.cs b/src/FluentValidation/Resources/StaticStringSource.cs index 6805f09d1..fe0a29431 100644 --- a/src/FluentValidation/Resources/StaticStringSource.cs +++ b/src/FluentValidation/Resources/StaticStringSource.cs @@ -38,7 +38,7 @@ public StaticStringSource(string message) { /// Construct the error message template /// /// Error message template - public string GetString(IValidationContext context) { + public string GetString(ICommonContext context) { return _message; } } diff --git a/src/FluentValidation/Syntax.cs b/src/FluentValidation/Syntax.cs index 8c61ce08d..10bbec829 100644 --- a/src/FluentValidation/Syntax.cs +++ b/src/FluentValidation/Syntax.cs @@ -103,7 +103,14 @@ public interface IRuleBuilderOptions : /// /// /// - public interface IRuleBuilderInitialCollection : IRuleBuilder, IConfigurable, IRuleBuilderInitialCollection> { + public interface IRuleBuilderInitialCollection : IRuleBuilder, IConfigurable, IRuleBuilderInitialCollection> { + + /// + /// Transforms the collection element value before validation occurs. + /// + /// + /// + IRuleBuilderInitial Transform(Func transformationFunc); } /// diff --git a/src/FluentValidation/Validators/AbstractComparisonValidator.cs b/src/FluentValidation/Validators/AbstractComparisonValidator.cs index a5a2a1128..6482f8af5 100644 --- a/src/FluentValidation/Validators/AbstractComparisonValidator.cs +++ b/src/FluentValidation/Validators/AbstractComparisonValidator.cs @@ -28,6 +28,7 @@ namespace FluentValidation.Validators { public abstract class AbstractComparisonValidator : PropertyValidator, IComparisonValidator { readonly Func _valueToCompareFunc; + private readonly string _comparisonMemberDisplayName; /// /// @@ -42,10 +43,12 @@ protected AbstractComparisonValidator(IComparable value, IStringSource errorSour /// /// /// + /// /// - protected AbstractComparisonValidator(Func valueToCompareFunc, MemberInfo member, IStringSource errorSource) : base(errorSource) { + protected AbstractComparisonValidator(Func valueToCompareFunc, MemberInfo member, string memberDisplayName, IStringSource errorSource) : base(errorSource) { this._valueToCompareFunc = valueToCompareFunc; this.MemberToCompare = member; + _comparisonMemberDisplayName = memberDisplayName; } /// @@ -64,7 +67,7 @@ protected sealed override bool IsValid(PropertyValidatorContext context) { if (!IsValid((IComparable)context.PropertyValue, value)) { context.MessageFormatter.AppendArgument("ComparisonValue", value); - context.MessageFormatter.AppendArgument("ComparisonProperty", MemberToCompare == null ? "" : MemberToCompare.Name.SplitPascalCase()); + context.MessageFormatter.AppendArgument("ComparisonProperty", _comparisonMemberDisplayName ?? ""); return false; } diff --git a/src/FluentValidation/Validators/AsyncPredicateValidator.cs b/src/FluentValidation/Validators/AsyncPredicateValidator.cs index 33445d2a7..7e1b6956a 100644 --- a/src/FluentValidation/Validators/AsyncPredicateValidator.cs +++ b/src/FluentValidation/Validators/AsyncPredicateValidator.cs @@ -50,7 +50,7 @@ protected override bool IsValid(PropertyValidatorContext context) { return Task.Run(() => IsValidAsync(context, new CancellationToken())).GetAwaiter().GetResult(); } - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { return context.IsAsync(); } } diff --git a/src/FluentValidation/Validators/AsyncValidatorBase.cs b/src/FluentValidation/Validators/AsyncValidatorBase.cs index f651039d0..f89aad110 100644 --- a/src/FluentValidation/Validators/AsyncValidatorBase.cs +++ b/src/FluentValidation/Validators/AsyncValidatorBase.cs @@ -30,7 +30,7 @@ namespace FluentValidation.Validators { /// Defines a property validator that can be run asynchronously. /// public abstract class AsyncValidatorBase : PropertyValidator { - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { return context.IsAsync() || Options.AsyncCondition != null; } diff --git a/src/FluentValidation/Validators/ChildValidatorAdaptor.cs b/src/FluentValidation/Validators/ChildValidatorAdaptor.cs index 64e013c46..18f42ac24 100644 --- a/src/FluentValidation/Validators/ChildValidatorAdaptor.cs +++ b/src/FluentValidation/Validators/ChildValidatorAdaptor.cs @@ -17,9 +17,9 @@ public interface IChildValidatorAdaptor { Type ValidatorType { get; } } - public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdaptor { - private readonly Func _validatorProvider; - private readonly IValidator _validator; + public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdaptor { + private readonly Func> _validatorProvider; + private readonly IValidator _validator; public Type ValidatorType { get; } @@ -27,12 +27,12 @@ public class ChildValidatorAdaptor : NoopPropertyValidator, IChildValidatorAdapt internal bool PassThroughParentContext { get; set; } - public ChildValidatorAdaptor(IValidator validator, Type validatorType) { + public ChildValidatorAdaptor(IValidator validator, Type validatorType) { _validator = validator; ValidatorType = validatorType; } - public ChildValidatorAdaptor(Func validatorProvider, Type validatorType) { + public ChildValidatorAdaptor(Func> validatorProvider, Type validatorType) { _validatorProvider = validatorProvider; ValidatorType = validatorType; } @@ -100,23 +100,24 @@ public override async Task> ValidateAsync(Propert return result.Errors; } - public virtual IValidator GetValidator(PropertyValidatorContext context) { + public virtual IValidator GetValidator(PropertyValidatorContext context) { context.Guard("Cannot pass a null context to GetValidator", nameof(context)); return _validatorProvider != null ? _validatorProvider(context) : _validator; } - protected ValidationContext CreateNewValidationContextForChildValidator(object instanceToValidate, PropertyValidatorContext context) { + protected IValidationContext CreateNewValidationContextForChildValidator(object instanceToValidate, PropertyValidatorContext context) { var selector = RuleSets?.Length > 0 ? new RulesetValidatorSelector(RuleSets) : null; - var newContext = context.ParentContext.CloneForChildValidator(instanceToValidate, PassThroughParentContext, selector); + var parentContext = ValidationContext.GetFromNonGenericContext(context.ParentContext); + var newContext = parentContext.CloneForChildValidator((TProperty)instanceToValidate, PassThroughParentContext, selector); - if(!context.ParentContext.IsChildCollectionContext) + if(!parentContext.IsChildCollectionContext) newContext.PropertyChain.Add(context.Rule.PropertyName); return newContext; } - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { return context.IsAsync() || Options.AsyncCondition != null; } diff --git a/src/FluentValidation/Validators/CustomValidator.cs b/src/FluentValidation/Validators/CustomValidator.cs index cf874e84a..2ce6d39fd 100644 --- a/src/FluentValidation/Validators/CustomValidator.cs +++ b/src/FluentValidation/Validators/CustomValidator.cs @@ -55,7 +55,7 @@ protected override bool IsValid(PropertyValidatorContext context) { throw new NotImplementedException(); } - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { return _isAsync && context.IsAsync(); } } @@ -63,7 +63,7 @@ public override bool ShouldValidateAsynchronously(ValidationContext context) { /// /// Custom validation context /// - public class CustomContext : IValidationContext { + public class CustomContext : ICommonContext { private PropertyValidatorContext _context; private List _failures = new List(); @@ -111,7 +111,7 @@ public void AddFailure(ValidationFailure failure) { public MessageFormatter MessageFormatter => _context.MessageFormatter; public object InstanceToValidate => _context.InstanceToValidate; public object PropertyValue => _context.PropertyValue; - IValidationContext IValidationContext.ParentContext => ParentContext; - public ValidationContext ParentContext => _context.ParentContext; + ICommonContext ICommonContext.ParentContext => ParentContext; + public IValidationContext ParentContext => _context.ParentContext; } } \ No newline at end of file diff --git a/src/FluentValidation/Validators/EqualValidator.cs b/src/FluentValidation/Validators/EqualValidator.cs index fd54736e1..af8b5c94f 100644 --- a/src/FluentValidation/Validators/EqualValidator.cs +++ b/src/FluentValidation/Validators/EqualValidator.cs @@ -26,6 +26,7 @@ namespace FluentValidation.Validators { public class EqualValidator : PropertyValidator, IComparisonValidator { readonly Func _func; + private readonly string _memberDisplayName; readonly IEqualityComparer _comparer; public EqualValidator(object valueToCompare, IEqualityComparer comparer = null) : base(new LanguageStringSource(nameof(EqualValidator))) { @@ -33,8 +34,9 @@ public EqualValidator(object valueToCompare, IEqualityComparer comparer = null) _comparer = comparer; } - public EqualValidator(Func comparisonProperty, MemberInfo member, IEqualityComparer comparer = null) : base(new LanguageStringSource(nameof(EqualValidator))) { + public EqualValidator(Func comparisonProperty, MemberInfo member, string memberDisplayName, IEqualityComparer comparer = null) : base(new LanguageStringSource(nameof(EqualValidator))) { _func = comparisonProperty; + _memberDisplayName = memberDisplayName; MemberToCompare = member; _comparer = comparer; } @@ -45,6 +47,8 @@ protected override bool IsValid(PropertyValidatorContext context) { if (!success) { context.MessageFormatter.AppendArgument("ComparisonValue", comparisonValue); + context.MessageFormatter.AppendArgument("ComparisonProperty", _memberDisplayName ?? ""); + return false; } diff --git a/src/FluentValidation/Validators/GreaterThanOrEqualValidator.cs b/src/FluentValidation/Validators/GreaterThanOrEqualValidator.cs index 41bd1fe77..8e2ba8f22 100644 --- a/src/FluentValidation/Validators/GreaterThanOrEqualValidator.cs +++ b/src/FluentValidation/Validators/GreaterThanOrEqualValidator.cs @@ -27,8 +27,8 @@ public GreaterThanOrEqualValidator(IComparable value) : base(value, new LanguageStringSource(nameof(GreaterThanOrEqualValidator))) { } - public GreaterThanOrEqualValidator(Func valueToCompareFunc, MemberInfo member) - : base(valueToCompareFunc, member, new LanguageStringSource(nameof(GreaterThanOrEqualValidator))) { + public GreaterThanOrEqualValidator(Func valueToCompareFunc, MemberInfo member, string memberDisplayName) + : base(valueToCompareFunc, member, memberDisplayName, new LanguageStringSource(nameof(GreaterThanOrEqualValidator))) { } public override bool IsValid(IComparable value, IComparable valueToCompare) { diff --git a/src/FluentValidation/Validators/GreaterThanValidator.cs b/src/FluentValidation/Validators/GreaterThanValidator.cs index fd1e257b6..7c0e74b6a 100644 --- a/src/FluentValidation/Validators/GreaterThanValidator.cs +++ b/src/FluentValidation/Validators/GreaterThanValidator.cs @@ -26,8 +26,8 @@ public class GreaterThanValidator : AbstractComparisonValidator { public GreaterThanValidator(IComparable value) : base(value, new LanguageStringSource(nameof(GreaterThanValidator))) { } - public GreaterThanValidator(Func valueToCompareFunc, MemberInfo member) - : base(valueToCompareFunc, member, new LanguageStringSource(nameof(GreaterThanValidator))) { + public GreaterThanValidator(Func valueToCompareFunc, MemberInfo member, string memberDisplayName) + : base(valueToCompareFunc, member, memberDisplayName, new LanguageStringSource(nameof(GreaterThanValidator))) { } public override bool IsValid(IComparable value, IComparable valueToCompare) { diff --git a/src/FluentValidation/Validators/IPropertyValidator.cs b/src/FluentValidation/Validators/IPropertyValidator.cs index 4e757b68d..cc4ab673e 100644 --- a/src/FluentValidation/Validators/IPropertyValidator.cs +++ b/src/FluentValidation/Validators/IPropertyValidator.cs @@ -50,7 +50,7 @@ public interface IPropertyValidator { /// /// /// - bool ShouldValidateAsynchronously(ValidationContext context); + bool ShouldValidateAsynchronously(IValidationContext context); /// /// Additional options for configuring the property validator. diff --git a/src/FluentValidation/Validators/LessThanOrEqualValidator.cs b/src/FluentValidation/Validators/LessThanOrEqualValidator.cs index 428cd39ad..9fd912ef1 100644 --- a/src/FluentValidation/Validators/LessThanOrEqualValidator.cs +++ b/src/FluentValidation/Validators/LessThanOrEqualValidator.cs @@ -19,15 +19,14 @@ namespace FluentValidation.Validators { using System; using System.Reflection; - using Internal; using Resources; public class LessThanOrEqualValidator : AbstractComparisonValidator { public LessThanOrEqualValidator(IComparable value) : base(value, new LanguageStringSource(nameof(LessThanOrEqualValidator))) { } - public LessThanOrEqualValidator(Func valueToCompareFunc, MemberInfo member) - : base(valueToCompareFunc, member, new LanguageStringSource(nameof(LessThanOrEqualValidator))) { + public LessThanOrEqualValidator(Func valueToCompareFunc, MemberInfo member, string memberDisplayName) + : base(valueToCompareFunc, member, memberDisplayName, new LanguageStringSource(nameof(LessThanOrEqualValidator))) { } public override bool IsValid(IComparable value, IComparable valueToCompare) { diff --git a/src/FluentValidation/Validators/LessThanValidator.cs b/src/FluentValidation/Validators/LessThanValidator.cs index 9f7e93670..9ed30551c 100644 --- a/src/FluentValidation/Validators/LessThanValidator.cs +++ b/src/FluentValidation/Validators/LessThanValidator.cs @@ -26,8 +26,8 @@ public class LessThanValidator : AbstractComparisonValidator { public LessThanValidator(IComparable value) : base(value, new LanguageStringSource(nameof(LessThanValidator))) { } - public LessThanValidator(Func valueToCompareFunc, MemberInfo member) - : base(valueToCompareFunc, member, new LanguageStringSource(nameof(LessThanValidator))) { + public LessThanValidator(Func valueToCompareFunc, MemberInfo member, string memberDisplayName) + : base(valueToCompareFunc, member, memberDisplayName, new LanguageStringSource(nameof(LessThanValidator))) { } public override bool IsValid(IComparable value, IComparable valueToCompare) { diff --git a/src/FluentValidation/Validators/NoopPropertyValidator.cs b/src/FluentValidation/Validators/NoopPropertyValidator.cs index 7ef10411c..d3fee9e46 100644 --- a/src/FluentValidation/Validators/NoopPropertyValidator.cs +++ b/src/FluentValidation/Validators/NoopPropertyValidator.cs @@ -34,7 +34,7 @@ public virtual System.Threading.Tasks.Task> Valid return Task.FromResult(Validate(context)); } - public virtual bool ShouldValidateAsynchronously(ValidationContext context) { + public virtual bool ShouldValidateAsynchronously(IValidationContext context) { return false; } diff --git a/src/FluentValidation/Validators/NotEqualValidator.cs b/src/FluentValidation/Validators/NotEqualValidator.cs index a65063ee1..2c7d827e8 100644 --- a/src/FluentValidation/Validators/NotEqualValidator.cs +++ b/src/FluentValidation/Validators/NotEqualValidator.cs @@ -26,10 +26,12 @@ namespace FluentValidation.Validators { public class NotEqualValidator : PropertyValidator, IComparisonValidator { private readonly IEqualityComparer _comparer; private readonly Func _func; + private readonly string _memberDisplayName; - public NotEqualValidator(Func func, MemberInfo memberToCompare, IEqualityComparer equalityComparer = null) : base(new LanguageStringSource(nameof(NotEqualValidator))) { + public NotEqualValidator(Func func, MemberInfo memberToCompare, string memberDisplayName, IEqualityComparer equalityComparer = null) : base(new LanguageStringSource(nameof(NotEqualValidator))) { _func = func; _comparer = equalityComparer; + _memberDisplayName = memberDisplayName; MemberToCompare = memberToCompare; } @@ -44,6 +46,7 @@ protected override bool IsValid(PropertyValidatorContext context) { if (!success) { context.MessageFormatter.AppendArgument("ComparisonValue", comparisonValue); + context.MessageFormatter.AppendArgument("ComparisonProperty", _memberDisplayName ?? ""); return false; } diff --git a/src/FluentValidation/Validators/OnFailureValidator.cs b/src/FluentValidation/Validators/OnFailureValidator.cs index c19c4da9e..3bad6c146 100644 --- a/src/FluentValidation/Validators/OnFailureValidator.cs +++ b/src/FluentValidation/Validators/OnFailureValidator.cs @@ -42,7 +42,7 @@ public override async Task> ValidateAsync(Propert return results; } - public override bool ShouldValidateAsynchronously(ValidationContext context) { + public override bool ShouldValidateAsynchronously(IValidationContext context) { // If the user has applied an async condition, or the inner validator requires async // validation then always go through the async path. if (Options.AsyncCondition != null || _innerValidator.ShouldValidateAsynchronously(context)) return true; diff --git a/src/FluentValidation/Validators/PropertyValidator.cs b/src/FluentValidation/Validators/PropertyValidator.cs index e6ae07194..f86bc54c1 100644 --- a/src/FluentValidation/Validators/PropertyValidator.cs +++ b/src/FluentValidation/Validators/PropertyValidator.cs @@ -60,7 +60,7 @@ public virtual async Task> ValidateAsync(Property } /// - public virtual bool ShouldValidateAsynchronously(ValidationContext context) { + public virtual bool ShouldValidateAsynchronously(IValidationContext context) { // If the user has applied an async condition, then always go through the async path // even if validator is being run synchronously. if (Options.AsyncCondition != null) return true; diff --git a/src/FluentValidation/Validators/PropertyValidatorContext.cs b/src/FluentValidation/Validators/PropertyValidatorContext.cs index 38509fd07..d2a460345 100644 --- a/src/FluentValidation/Validators/PropertyValidatorContext.cs +++ b/src/FluentValidation/Validators/PropertyValidatorContext.cs @@ -20,11 +20,11 @@ namespace FluentValidation.Validators { using System; using Internal; - public class PropertyValidatorContext : IValidationContext { + public class PropertyValidatorContext : ICommonContext { private MessageFormatter _messageFormatter; private readonly Lazy _propertyValueContainer; - public ValidationContext ParentContext { get; private set; } + public IValidationContext ParentContext { get; private set; } public PropertyRule Rule { get; private set; } public string PropertyName { get; private set; } @@ -38,9 +38,9 @@ public class PropertyValidatorContext : IValidationContext { public object PropertyValue => _propertyValueContainer.Value; // Explicit implementation so we don't have to expose the base interface. - IValidationContext IValidationContext.ParentContext => ParentContext; + ICommonContext ICommonContext.ParentContext => ParentContext; - public PropertyValidatorContext(ValidationContext parentContext, PropertyRule rule, string propertyName) { + public PropertyValidatorContext(IValidationContext parentContext, PropertyRule rule, string propertyName) { ParentContext = parentContext; Rule = rule; PropertyName = propertyName; @@ -51,7 +51,7 @@ public PropertyValidatorContext(ValidationContext parentContext, PropertyRule ru }); } - public PropertyValidatorContext(ValidationContext parentContext, PropertyRule rule, string propertyName, object propertyValue) { + public PropertyValidatorContext(IValidationContext parentContext, PropertyRule rule, string propertyName, object propertyValue) { ParentContext = parentContext; Rule = rule; PropertyName = propertyName;