├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── README.md └── workflows │ ├── ci.yml │ └── lock.yml ├── .gitignore ├── Build.cmd ├── FluentValidation.AspNetCore.sln ├── License.txt ├── after.FluentValidation.AspNetCore.sln.targets ├── build.ps1 ├── build.sh ├── global.json ├── logo └── fluent-validation-icon.png ├── nuget.config └── src ├── FluentValidation-Release.snk ├── FluentValidation.AspNetCore ├── Adapters │ ├── ClientValidatorBase.cs │ ├── CreditCardClientValidator.cs │ ├── EmailClientValidator.cs │ ├── EqualToClientValidator.cs │ ├── MaxLengthClientValidator.cs │ ├── MinLengthClientValidator.cs │ ├── RangeClientValidator.cs │ ├── RangeMaxClientValidator.cs │ ├── RangeMinClientValidator.cs │ ├── RegexClientValidator.cs │ ├── RequiredClientValidator.cs │ └── StringLengthClientValidator.cs ├── AssemblyInfo.cs ├── CustomizeValidatorAttribute.cs ├── FluentValidation.AspNetCore.csproj ├── FluentValidation.AspNetCore.csproj.DotSettings ├── FluentValidationBindingMetadataProvider.cs ├── FluentValidationClientModelValidatorProvider.cs ├── FluentValidationModelValidatorProvider.cs ├── FluentValidationMvcConfiguration.cs ├── FluentValidationMvcExtensions.cs ├── FluentValidationObjectModelValidator.cs ├── FluentValidationVisitor.cs ├── IValidatorInterceptor.cs ├── MvcValidationHelper.cs ├── README.md ├── RuleSetForClientSideMessagesAttribute.cs ├── ValidationResultExtensions.cs └── ValidatorDescriptorCache.cs └── FluentValidation.Tests.AspNetCore ├── ClientsideMessageTester.cs ├── Controllers ├── ApiTestController.cs ├── ClientsideController.cs ├── HomeController.cs └── TestController.cs ├── CultureScope.cs ├── DependencyInjectionTests.cs ├── DisableAutoValidationTests.cs ├── DisableDataAnnotationsTests.cs ├── ExtensionTests.cs ├── FluentValidation.Tests.AspNetCore.csproj ├── FluentValidationModelValidatorFilterIntegrationTests.cs ├── GlobalInterceptorTests.cs ├── HttpClientExtensions.cs ├── ImplicitRootCollectionTests.cs ├── ImplicitValidationTests.cs ├── MvcIntegrationTests.cs ├── Pages ├── RulesetTest.cshtml ├── Rulesets │ ├── DefaultAndSpecifiedRuleSet.cshtml │ ├── DefaultRuleSet.cshtml │ ├── MultipleRuleSets.cshtml │ ├── RuleSetForHandlers.cshtml │ └── SpecifiedRuleSet.cshtml ├── TestPage1.cshtml ├── TestPageWithPrefix.cshtml └── _ViewImports.cshtml ├── RazorPagesTests.cs ├── ServiceCollectionExtensionTests.cs ├── ServiceProviderTests.cs ├── Startup.cs ├── TestExtensions.cs ├── TestMessages.Designer.cs ├── TestMessages.resx ├── TestModels.cs ├── TestModels_ClientSIde.cs ├── TestPageModel.cs ├── TypeFilterTests.cs ├── Views ├── Clientside │ ├── Inputs.cshtml │ ├── RuleSet.cshtml │ └── _TestPartial.cshtml └── _ViewImports.cshtml ├── WebAppFixture.cs └── xunit.runner.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace=true 8 | insert_final_newline=true 9 | 10 | # Microsoft .NET properties 11 | csharp_new_line_before_members_in_object_initializers=false 12 | csharp_new_line_before_open_brace=none 13 | csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion 14 | csharp_style_var_elsewhere=true:hint 15 | csharp_style_var_for_built_in_types=true:hint 16 | csharp_style_var_when_type_is_apparent=true:hint 17 | dotnet_style_predefined_type_for_locals_parameters_members=true:hint 18 | dotnet_style_predefined_type_for_member_access=true:hint 19 | dotnet_style_qualification_for_event=false:warning 20 | dotnet_style_qualification_for_field=false:warning 21 | dotnet_style_qualification_for_method=false:warning 22 | dotnet_style_qualification_for_property=false:warning 23 | dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint 24 | 25 | # ReSharper properties 26 | resharper_add_imports_to_deepest_scope=true 27 | resharper_autodetect_indent_settings=true 28 | resharper_braces_redundant=false 29 | resharper_csharp_indent_style=tab 30 | resharper_csharp_wrap_lines=false 31 | resharper_place_accessorholder_attribute_on_same_line=False 32 | resharper_use_indent_from_vs=false 33 | resharper_xmldoc_indent_child_elements=RemoveIndent 34 | resharper_xmldoc_indent_text=RemoveIndent 35 | resharper_xmldoc_wrap_lines=false 36 | 37 | [*.cs] 38 | indent_style = tab 39 | tab_width = 2 40 | indent_size = tab 41 | end_of_line = crlf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs -text diff=csharp 17 | *.vb -text 18 | *.c -text 19 | *.cpp -text 20 | *.cxx -text 21 | *.h -text 22 | *.hxx -text 23 | *.py -text 24 | *.rb -text 25 | *.java -text 26 | *.html -text 27 | *.htm -text 28 | *.css -text 29 | *.scss -text 30 | *.sass -text 31 | *.less -text 32 | *.js -text 33 | *.lisp -text 34 | *.clj -text 35 | *.sql -text 36 | *.php -text 37 | *.lua -text 38 | *.m -text 39 | *.asm -text 40 | *.erl -text 41 | *.fs -text 42 | *.fsx -text 43 | *.hs -text 44 | 45 | *.csproj -text merge=union 46 | *.vbproj -text merge=union 47 | *.fsproj -text merge=union 48 | *.dbproj -text merge=union 49 | *.sln -text merge=union 50 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | FluentValidation has adopted the code of conduct defined by the Contributor Covenant, which can be found on the [.NET Foundation website](http://www.dotnetfoundation.org/code-of-conduct). 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## General support, feedback, and discussions 2 | FluentValidation is maintained on a voluntary basis, and unfortunately this means we are unable to provide general support or answer questions on usage due to the time and effort required to moderate these. The issue tracker should only be used for bug reports in the core FluentValidation library, or feature requests where appropriate. Requests for general support or questions on usage will be closed. We appreciate that this may appear strict, but is necessary to protect the free time and mental health of the project's maintainers. Thank you for understanding. 3 | 4 | ## Supporting the project 5 | If you use FluentValidation in a commercial project, please sponsor the project financially. FluentValidation is developed and supported by [@JeremySkinner](https://github.com/JeremySkinner) for free in his spare time and financial sponsorship helps keep the project going. You can sponsor the project via either [GitHub sponsors](https://github.com/sponsors/JeremySkinner) or [OpenCollective](https://opencollective.com/FluentValidation). 6 | 7 | 8 | ## Filing bug reports and feature requests 9 | The best way to get your bug fixed is to be as detailed as you can be about the problem. 10 | 11 | Please check both the documentation at https://fluentvalidation.net and old issues first to see if your question has already been answered. 12 | 13 | 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. Issues that don't provide enough information to reproduce will be closed. 14 | 15 | Please ensure all sample code is properly formatted and readable (GitHub supports [markdown](https://github.github.com/github-flavored-markdown/)). Issues that don't include all necessary code (or a sample project) to reproduce will be closed. 16 | 17 | We do our best to respond to all bug reports and feature requests, but FluentValidation is maintained on a voluntary basis and we cannot guarantee how quickly these will be looked at. 18 | 19 | ## Contributing Code 20 | Please open an issue to discuss new feature requests before submitting a Pull Request. This allows the maintainers to discuss whether your feature is a suitable fit for the project before any code is written. Please don't open a pull request without first discussing whether the feature fits with the project roadmap. 21 | 22 | ## Building the code 23 | Run `Build.cmd` (windows) or build.sh (Linux/mac) from the command line. This builds the project and runs tests. Building requires the following software to be installed: 24 | 25 | * Windows Powershell or Powershell Core 26 | * .NET Core 3.1 SDK 27 | * .NET Core 2.1 SDK 28 | * .NET 5 SDK 29 | 30 | ## Contributing code and content 31 | You will need to sign a [Contributor License Agreement](https://cla.dotnetfoundation.org/) before submitting your pull request. 32 | 33 | Make sure you can build the code. Familiarize yourself with the project workflow and our coding conventions. If you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests. 34 | 35 | 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. 36 | 37 | Tests must be provided for all pull requests that add or change functionality. 38 | 39 | Please ensure that you follow the existing code-style when adding new code to the project. This may seem pedantic, but it makes it much easier to review pull requests when contributed code matches the existing project style. Specifically: 40 | - Please ensure that your editor is configured to use tabs for indentation, not spaces 41 | - Please ensure that the project copyright notice is included in the header for all files. 42 | - Please ensure `using` statements are inside the namespace declaration 43 | - Please ensure that all opening braces are on the end of line: 44 | 45 | ```csharp 46 | // Opening braces should be on the end of the line like this 47 | if (foo) { 48 | 49 | } 50 | 51 | // Not like this: 52 | if (foo) 53 | { 54 | 55 | } 56 | ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JeremySkinner] 4 | open_collective: FluentValidation 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "Report a reproducible bug in FluentValidation. Also use this template if you're having trouble upgrading from version 10.x 3 | to 11.x" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to file a bug report! Before filling out a new issue, please first check to see if your question has been answered in an existing issue, [in the readme](https://github.com/FluentValidation/FluentValidation.AspNetCore#aspnet-core-integration-for-fluentvalidation) or in the documentation for the main FluentValidation project at [https://docs.fluentvalidation.net](https://docs.fluentvalidation.net) 9 | 10 | **Please note** FluentValidation is supported for free on a voluntary basis. If you use FluentValidation please [sponsor the project](https://github.com/sponsors/JeremySkinner) so that its development can continue. Bug reports from sponsors will be prioritised. 11 | 12 | Please take the time to fill in this form as completely as possible. If you leave out sections there is a high likelihood your issue will be closed. 13 | - type: input 14 | attributes: 15 | label: FluentValidation.AspNetCore version 16 | description: Which version of FluentValidation.AspNetCore are you using? 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: FluentValidation version 22 | description: Which version of FluentValidation are you using? 23 | validations: 24 | required: true 25 | - type: dropdown 26 | attributes: 27 | label: ASP.NET version 28 | description: Please select which version of ASP.NET Core you're running 29 | options: 30 | - .NET 7 (Preview) 31 | - .NET 6 32 | - .NET 5 33 | - .NET Core 3.1 34 | - An older version 35 | validations: 36 | required: true 37 | - type: textarea 38 | attributes: 39 | label: Summary 40 | description: A clear and concise description of the issue that you're having. 41 | validations: 42 | required: true 43 | - type: textarea 44 | attributes: 45 | label: Steps to Reproduce 46 | description: "Please include all the steps necessary to reproduce the issue, including any sample code (preferably in the form of a runnable unit test). Alternatively, please provide a link to a *minimal* sample project. Please do not include screenshots of code." 47 | validations: 48 | required: true 49 | - type: markdown 50 | attributes: 51 | value: | 52 | Please ensure that any code samples are readable and [properly formatted as code blocks](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). 53 | 54 | **Please realise that it is up to you to debug your code thoroughly. Be as certain as possible that the bug is with FluentValidation, and not with your own code, prior to opening an issue.** 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: General Support and Usage questions 4 | url: "https://github.com/FluentValidation/FluentValidation.AspNetCore/blob/main/.github/CONTRIBUTING.md" 5 | about: We are unable to provide general support for usage of the library 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F64B Feature Request" 2 | description: 'Want us to add something to FluentValidation?' 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for helping us make FluentValidation even better! Please fill out this form as thoroughly as possible. 8 | 9 | **Please note** FluentValidation is supported for free on a voluntary basis. If you use FluentValidation please [sponsor the project](https://github.com/sponsors/JeremySkinner) so that its development can continue. Feature requests from sponsors will be prioritised. 10 | - type: textarea 11 | attributes: 12 | label: Is your feature request related to a problem? Please describe. 13 | description: A clear and concise description of what the problem is. 14 | validations: 15 | required: false 16 | - type: textarea 17 | attributes: 18 | label: Describe the solution you'd like 19 | description: A clear and concise description of what you want to happen. 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered 25 | description: A clear and concise description of any alternative solutions or features you've considered. 26 | - type: textarea 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context or sample code about the feature request here. 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: ['windows-latest', 'ubuntu-latest'] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup dotnet 24 | uses: actions/setup-dotnet@v2 25 | with: 26 | dotnet-version: | 27 | 9.0.x 28 | 7.0.100 29 | 6.0.x 30 | 31 | - name: Build and Test 32 | run: ./build.ps1 33 | shell: pwsh 34 | env: 35 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 36 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock closed issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | lock: 12 | permissions: 13 | issues: write # for dessant/lock-threads to lock issues 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: dessant/lock-threads@v2 17 | with: 18 | github-token: ${{ github.token }} 19 | process-only: 'issues' 20 | issue-lock-inactive-days: '14' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.suo 3 | *.user 4 | bin 5 | Bin 6 | obj 7 | _ReSharper* 8 | *.csproj.user 9 | *.resharper.user 10 | *.suo 11 | *.cache 12 | TestResult.xml 13 | *.orig 14 | .hg/ 15 | .hgignore 16 | packages/ 17 | *.sln.ide/ 18 | artifacts/ 19 | TestResults/ 20 | .vs/ 21 | *.lock.json 22 | *.ncrunch* 23 | .idea/ 24 | .build/ 25 | docs/_site/ 26 | docs/Gemfile.lock 27 | docs/_build/ 28 | src/FluentValidation.Tests.Benchmarks/BenchmarkDotNet.Artifacts/ 29 | -------------------------------------------------------------------------------- /Build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -noprofile -ExecutionPolicy Unrestricted -File build.ps1 %* -------------------------------------------------------------------------------- /FluentValidation.AspNetCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3906F280-A567-4AD7-A0EF-7253E95E7852}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentValidation.AspNetCore", "src\FluentValidation.AspNetCore\FluentValidation.AspNetCore.csproj", "{D8A44C11-51B8-4AA1-9391-C4F89078181C}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentValidation.Tests.AspNetCore", "src\FluentValidation.Tests.AspNetCore\FluentValidation.Tests.AspNetCore.csproj", "{3DFE3035-9685-4B76-AF21-074BCD8F69B7}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {D8A44C11-51B8-4AA1-9391-C4F89078181C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {D8A44C11-51B8-4AA1-9391-C4F89078181C}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {D8A44C11-51B8-4AA1-9391-C4F89078181C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D8A44C11-51B8-4AA1-9391-C4F89078181C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {3DFE3035-9685-4B76-AF21-074BCD8F69B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {3DFE3035-9685-4B76-AF21-074BCD8F69B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {3DFE3035-9685-4B76-AF21-074BCD8F69B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {3DFE3035-9685-4B76-AF21-074BCD8F69B7}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {33CD33AF-2F26-44A0-BA94-F9299272362B} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /after.FluentValidation.AspNetCore.sln.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Release 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$configuration = 'Release', 3 | [string]$path = $PSScriptRoot, 4 | [string[]]$targets = 'default' 5 | ) 6 | 7 | $ErrorActionPreference = "Stop" 8 | 9 | # Boostrap posh-build 10 | $build_dir = Join-Path $path ".build" 11 | if (! (Test-Path (Join-Path $build_dir "Posh-Build.ps1"))) { 12 | Write-Host "Installing posh-build..."; New-Item -Type Directory $build_dir -ErrorAction Ignore | Out-Null; 13 | (New-Object Net.WebClient).DownloadFile('https://raw.githubusercontent.com/jeremyskinner/posh-build/master/Posh-Build.ps1', "$build_dir/Posh-Build.ps1") 14 | } 15 | . (Join-Path $build_dir "Posh-Build.ps1") 16 | 17 | # Set these variables as desired 18 | $packages_dir = Join-Path $build_dir "packages" 19 | $output_dir = Join-Path $build_dir $configuration 20 | $solution_file = Join-Path $path "FluentValidation.AspNetCore.sln" 21 | 22 | target default -depends compile, test, deploy 23 | 24 | target compile { 25 | Invoke-Dotnet build $solution_file -c $configuration --no-incremental 26 | } 27 | 28 | target test { 29 | Invoke-Dotnet test $solution_file -c $configuration --no-build --logger trx 30 | } 31 | 32 | target deploy { 33 | Remove-Item $packages_dir -Force -Recurse -ErrorAction Ignore 2> $null 34 | Remove-Item $output_dir -Force -Recurse -ErrorAction Ignore 2> $null 35 | 36 | Invoke-Dotnet pack $solution_file -c $configuration 37 | } 38 | 39 | target publish { 40 | $key = $Env:NUGET_API_KEY 41 | Nuget-Push -directory $packages_dir -key $key -prompt $true 42 | } 43 | 44 | Start-Build $targets 45 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | pwsh -noprofile ./build.ps1 $@ 2 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /logo/fluent-validation-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FluentValidation/FluentValidation.AspNetCore/e891499e1e83dcc1310076ff10dc8a2bcc739563/logo/fluent-validation-icon.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/FluentValidation-Release.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FluentValidation/FluentValidation.AspNetCore/e891499e1e83dcc1310076ff10dc8a2bcc739563/src/FluentValidation-Release.snk -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/ClientValidatorBase.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using System.ComponentModel; 23 | using Internal; 24 | using Validators; 25 | using System.Linq; 26 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 27 | 28 | public abstract class ClientValidatorBase : IClientModelValidator { 29 | public IPropertyValidator Validator { get; } 30 | public IValidationRule Rule { get; } 31 | public IRuleComponent Component { get; } 32 | 33 | public ClientValidatorBase(IValidationRule rule, IRuleComponent component) { 34 | Component = component; 35 | Validator = component.Validator; 36 | Rule = rule; 37 | } 38 | 39 | public abstract void AddValidation(ClientModelValidationContext context); 40 | 41 | protected static bool MergeAttribute(IDictionary attributes, string key, string value) { 42 | if (attributes.ContainsKey(key)) { 43 | return false; 44 | } 45 | 46 | attributes.Add(key, value); 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/CreditCardClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using Internal; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 24 | using Resources; 25 | using Validators; 26 | 27 | internal class CreditCardClientValidator : ClientValidatorBase { 28 | public CreditCardClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 29 | } 30 | 31 | public override void AddValidation(ClientModelValidationContext context) { 32 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 33 | var formatter = cfg.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName(null)); 34 | string message; 35 | try { 36 | message = Component.GetUnformattedErrorMessage(); 37 | } 38 | catch (NullReferenceException) { 39 | message = cfg.LanguageManager.GetString("CreditCardValidator"); 40 | } 41 | message = formatter.BuildMessage(message); 42 | MergeAttribute(context.Attributes, "data-val", "true"); 43 | MergeAttribute(context.Attributes, "data-val-creditcard", message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/EmailClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using Internal; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 24 | using Resources; 25 | using Validators; 26 | 27 | internal class EmailClientValidator : ClientValidatorBase { 28 | 29 | public EmailClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 30 | } 31 | 32 | public override void AddValidation(ClientModelValidationContext context) { 33 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 34 | var formatter = cfg.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName(null)); 35 | 36 | string messageTemplate; 37 | try { 38 | messageTemplate = Component.GetUnformattedErrorMessage(); 39 | } 40 | catch (NullReferenceException) { 41 | messageTemplate = cfg.LanguageManager.GetString("EmailValidator"); 42 | } 43 | 44 | string message = formatter.BuildMessage(messageTemplate); 45 | MergeAttribute(context.Attributes, "data-val", "true"); 46 | MergeAttribute(context.Attributes, "data-val-email", message); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/EqualToClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using System.Reflection; 23 | using Internal; 24 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 25 | using Resources; 26 | using Validators; 27 | 28 | internal class EqualToClientValidator : ClientValidatorBase { 29 | IComparisonValidator EqualValidator => (IComparisonValidator)Validator; 30 | 31 | public EqualToClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 32 | } 33 | 34 | public override void AddValidation(ClientModelValidationContext context) { 35 | 36 | var propertyToCompare = EqualValidator.MemberToCompare as PropertyInfo; 37 | 38 | if (propertyToCompare != null) { 39 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 40 | // If propertyToCompare is not null then we're comparing to another property. 41 | // If propertyToCompare is null then we're either comparing against a literal value, a field or a method call. 42 | // We only care about property comparisons in this case. 43 | 44 | var comparisonDisplayName = 45 | cfg.DisplayNameResolver(Rule.TypeToValidate, propertyToCompare, null) 46 | ?? propertyToCompare.Name.SplitPascalCase(); 47 | 48 | var formatter = cfg.MessageFormatterFactory() 49 | .AppendPropertyName(Rule.GetDisplayName(null)) 50 | .AppendArgument("ComparisonValue", comparisonDisplayName); 51 | 52 | string messageTemplate; 53 | try { 54 | messageTemplate = Component.GetUnformattedErrorMessage(); 55 | } 56 | catch (NullReferenceException) { 57 | messageTemplate = cfg.LanguageManager.GetString("EqualValidator"); 58 | } 59 | string message = formatter.BuildMessage(messageTemplate); 60 | MergeAttribute(context.Attributes, "data-val", "true"); 61 | MergeAttribute(context.Attributes, "data-val-equalto", message); 62 | MergeAttribute(context.Attributes, "data-val-equalto-other", "*." + propertyToCompare.Name); 63 | } 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/MaxLengthClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | namespace FluentValidation.AspNetCore; 22 | 23 | using System; 24 | using Internal; 25 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 26 | using Resources; 27 | using Validators; 28 | 29 | internal class MaxLengthClientValidator : ClientValidatorBase { 30 | public override void AddValidation(ClientModelValidationContext context) { 31 | var lengthVal = (ILengthValidator) Validator; 32 | 33 | MergeAttribute(context.Attributes, "data-val", "true"); 34 | MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(lengthVal, context)); 35 | MergeAttribute(context.Attributes, "data-val-maxlength-max", lengthVal.Max.ToString()); 36 | } 37 | 38 | private string GetErrorMessage(ILengthValidator lengthVal, ClientModelValidationContext context) { 39 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 40 | 41 | var formatter = cfg.MessageFormatterFactory() 42 | .AppendPropertyName(Rule.GetDisplayName(null)) 43 | .AppendArgument("MinLength", lengthVal.Min) 44 | .AppendArgument("MaxLength", lengthVal.Max); 45 | 46 | string message; 47 | try { 48 | message = Component.GetUnformattedErrorMessage(); 49 | } 50 | catch (NullReferenceException) { 51 | message = cfg.LanguageManager.GetString("MaximumLength_Simple"); 52 | } 53 | 54 | if (message.Contains("{TotalLength}")) { 55 | message = cfg.LanguageManager.GetString("MaximumLength_Simple"); 56 | } 57 | 58 | message = formatter.BuildMessage(message); 59 | return message; 60 | } 61 | 62 | public MaxLengthClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/MinLengthClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | namespace FluentValidation.AspNetCore; 22 | 23 | using System; 24 | using Internal; 25 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 26 | using Resources; 27 | using Validators; 28 | 29 | internal class MinLengthClientValidator : ClientValidatorBase { 30 | public override void AddValidation(ClientModelValidationContext context) { 31 | var lengthVal = (ILengthValidator) Validator; 32 | 33 | MergeAttribute(context.Attributes, "data-val", "true"); 34 | MergeAttribute(context.Attributes, "data-val-minlength", GetErrorMessage(lengthVal, context)); 35 | MergeAttribute(context.Attributes, "data-val-minlength-min", lengthVal.Min.ToString()); 36 | } 37 | 38 | private string GetErrorMessage(ILengthValidator lengthVal, ClientModelValidationContext context) { 39 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 40 | 41 | var formatter = cfg.MessageFormatterFactory() 42 | .AppendPropertyName(Rule.GetDisplayName(null)) 43 | .AppendArgument("MinLength", lengthVal.Min) 44 | .AppendArgument("MaxLength", lengthVal.Max); 45 | 46 | string message; 47 | try { 48 | message = Component.GetUnformattedErrorMessage(); 49 | } 50 | catch (NullReferenceException) { 51 | message = cfg.LanguageManager.GetString("MinimumLength_Simple"); 52 | } 53 | 54 | if (message.Contains("{TotalLength}")) { 55 | message = cfg.LanguageManager.GetString("MinimumLength_Simple"); 56 | } 57 | 58 | message = formatter.BuildMessage(message); 59 | return message; 60 | } 61 | 62 | public MinLengthClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/RangeClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using Internal; 21 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 22 | using Resources; 23 | using System; 24 | using System.Globalization; 25 | using Validators; 26 | 27 | internal class RangeClientValidator : ClientValidatorBase { 28 | IBetweenValidator RangeValidator => (IBetweenValidator)Validator; 29 | 30 | public RangeClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 31 | 32 | } 33 | 34 | public override void AddValidation(ClientModelValidationContext context) { 35 | if (RangeValidator.To != null && RangeValidator.From != null) { 36 | MergeAttribute(context.Attributes, "data-val", "true"); 37 | MergeAttribute(context.Attributes, "data-val-range", GetErrorMessage(context)); 38 | MergeAttribute(context.Attributes, "data-val-range-max", Convert.ToString(RangeValidator.To, CultureInfo.InvariantCulture)); 39 | MergeAttribute(context.Attributes, "data-val-range-min", Convert.ToString(RangeValidator.From, CultureInfo.InvariantCulture)); 40 | } 41 | } 42 | 43 | private string GetErrorMessage(ClientModelValidationContext context) { 44 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 45 | 46 | var formatter = cfg.MessageFormatterFactory() 47 | .AppendPropertyName(Rule.GetDisplayName(null)) 48 | .AppendArgument("From", RangeValidator.From) 49 | .AppendArgument("To", RangeValidator.To); 50 | 51 | string message; 52 | 53 | try { 54 | message = Component.GetUnformattedErrorMessage(); 55 | } 56 | catch (NullReferenceException) { 57 | message = cfg.LanguageManager.GetString("InclusiveBetween_Simple"); 58 | } 59 | 60 | if (message.Contains("{PropertyValue}")) { 61 | message = cfg.LanguageManager.GetString("InclusiveBetween_Simple"); 62 | } 63 | message = formatter.BuildMessage(message); 64 | 65 | return message; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/RangeMaxClientValidator.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.AspNetCore; 2 | 3 | using Internal; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 5 | using Resources; 6 | using System; 7 | using System.Globalization; 8 | using Validators; 9 | 10 | internal class RangeMaxClientValidator : ClientValidatorBase { 11 | IComparisonValidator RangeValidator => (IComparisonValidator)Validator; 12 | 13 | public RangeMaxClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 14 | } 15 | 16 | public override void AddValidation(ClientModelValidationContext context) { 17 | var compareValue = RangeValidator.ValueToCompare; 18 | 19 | if (compareValue != null) { 20 | MergeAttribute(context.Attributes, "data-val", "true"); 21 | MergeAttribute(context.Attributes, "data-val-range", GetErrorMessage(context)); 22 | MergeAttribute(context.Attributes, "data-val-range-max", Convert.ToString(compareValue, CultureInfo.InvariantCulture)); 23 | MergeAttribute(context.Attributes, "data-val-range-min", "0"); 24 | } 25 | } 26 | 27 | private string GetErrorMessage(ClientModelValidationContext context) { 28 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 29 | 30 | var formatter = cfg.MessageFormatterFactory() 31 | .AppendPropertyName(Rule.GetDisplayName(null)) 32 | .AppendArgument("ComparisonValue", RangeValidator.ValueToCompare); 33 | 34 | string message; 35 | 36 | try { 37 | message = Component.GetUnformattedErrorMessage(); 38 | } 39 | catch (NullReferenceException) { 40 | message = cfg.LanguageManager.GetString("LessThanOrEqualValidator"); 41 | } 42 | 43 | message = formatter.BuildMessage(message); 44 | 45 | return message; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/RangeMinClientValidator.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.AspNetCore; 2 | 3 | using Internal; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 5 | using Resources; 6 | using System; 7 | using System.Globalization; 8 | using Validators; 9 | 10 | internal class RangeMinClientValidator : ClientValidatorBase { 11 | IComparisonValidator RangeValidator => (IComparisonValidator)Validator; 12 | 13 | public RangeMinClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 14 | 15 | } 16 | 17 | public override void AddValidation(ClientModelValidationContext context) { 18 | var compareValue = RangeValidator.ValueToCompare; 19 | 20 | if (compareValue != null) { 21 | MergeAttribute(context.Attributes, "data-val", "true"); 22 | MergeAttribute(context.Attributes, "data-val-range", GetErrorMessage(context)); 23 | MergeAttribute(context.Attributes, "data-val-range-min", Convert.ToString(compareValue, CultureInfo.InvariantCulture)); 24 | } 25 | } 26 | 27 | private string GetErrorMessage(ClientModelValidationContext context) { 28 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 29 | 30 | var formatter = cfg.MessageFormatterFactory() 31 | .AppendPropertyName(Rule.GetDisplayName(null)) 32 | .AppendArgument("ComparisonValue", RangeValidator.ValueToCompare); 33 | 34 | string message; 35 | 36 | try { 37 | message = Component.GetUnformattedErrorMessage(); 38 | } 39 | catch (NullReferenceException) { 40 | message = cfg.LanguageManager.GetString("GreaterThanOrEqualValidator"); 41 | } 42 | 43 | message = formatter.BuildMessage(message); 44 | 45 | return message; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/RegexClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using Internal; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 24 | using Resources; 25 | using Validators; 26 | 27 | internal class RegexClientValidator : ClientValidatorBase { 28 | 29 | public RegexClientValidator(IValidationRule rule, IRuleComponent component) 30 | : base(rule, component) { 31 | } 32 | 33 | public override void AddValidation(ClientModelValidationContext context) { 34 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 35 | var regexVal = (IRegularExpressionValidator)Validator; 36 | var formatter = cfg.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName(null)); 37 | string messageTemplate; 38 | try { 39 | messageTemplate = Component.GetUnformattedErrorMessage(); 40 | } 41 | catch (NullReferenceException) { 42 | messageTemplate = cfg.LanguageManager.GetString("RegularExpressionValidator"); 43 | } 44 | 45 | string message = formatter.BuildMessage(messageTemplate); 46 | 47 | MergeAttribute(context.Attributes, "data-val", "true"); 48 | MergeAttribute(context.Attributes, "data-val-regex", message); 49 | MergeAttribute(context.Attributes, "data-val-regex-pattern", regexVal.Expression); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/RequiredClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using Internal; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 24 | using Resources; 25 | using Validators; 26 | 27 | internal class RequiredClientValidator : ClientValidatorBase{ 28 | public RequiredClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { 29 | 30 | } 31 | 32 | public override void AddValidation(ClientModelValidationContext context) { 33 | MergeAttribute(context.Attributes, "data-val", "true"); 34 | MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context)); 35 | } 36 | 37 | private string GetErrorMessage(ClientModelValidationContext context) { 38 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 39 | var formatter = cfg.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName(null)); 40 | string messageTemplate; 41 | try { 42 | messageTemplate = Component.GetUnformattedErrorMessage(); 43 | } 44 | catch (NullReferenceException) { 45 | messageTemplate = cfg.LanguageManager.GetString("NotEmptyValidator"); 46 | } 47 | 48 | var message = formatter.BuildMessage(messageTemplate); 49 | return message; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/Adapters/StringLengthClientValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using Internal; 22 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 23 | using Validators; 24 | 25 | internal class StringLengthClientValidator : ClientValidatorBase { 26 | public StringLengthClientValidator(IValidationRule rule, IRuleComponent component) 27 | : base(rule, component) { 28 | } 29 | 30 | public override void AddValidation(ClientModelValidationContext context) { 31 | var lengthVal = (ILengthValidator)Validator; 32 | 33 | MergeAttribute(context.Attributes, "data-val", "true"); 34 | MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(lengthVal, context)); 35 | MergeAttribute(context.Attributes, "data-val-length-max", lengthVal.Max.ToString()); 36 | MergeAttribute(context.Attributes, "data-val-length-min", lengthVal.Min.ToString()); 37 | } 38 | 39 | private string GetErrorMessage(ILengthValidator lengthVal, ClientModelValidationContext context) { 40 | var cfg = context.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 41 | 42 | var formatter = cfg.MessageFormatterFactory() 43 | .AppendPropertyName(Rule.GetDisplayName(null)) 44 | .AppendArgument("MinLength", lengthVal.Min) 45 | .AppendArgument("MaxLength", lengthVal.Max); 46 | 47 | string message; 48 | try { 49 | message = Component.GetUnformattedErrorMessage(); 50 | } 51 | catch (NullReferenceException) { 52 | if (lengthVal is IExactLengthValidator) { 53 | message = cfg.LanguageManager.GetString("ExactLength_Simple"); 54 | } 55 | else { 56 | message = cfg.LanguageManager.GetString("Length_Simple"); 57 | } 58 | } 59 | 60 | 61 | if (message.Contains("{TotalLength}")) { 62 | if (lengthVal is IExactLengthValidator) { 63 | message = cfg.LanguageManager.GetString("ExactLength_Simple"); 64 | } 65 | else { 66 | message = cfg.LanguageManager.GetString("Length_Simple"); 67 | } 68 | } 69 | 70 | message = formatter.BuildMessage(message); 71 | return message; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | [assembly : AssemblyVersion("11.0.0.0")] 5 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/CustomizeValidatorAttribute.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System; 22 | using FluentValidation.Internal; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 24 | using Microsoft.Extensions.DependencyInjection; 25 | using System.Linq; 26 | 27 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] 28 | public class CustomizeValidatorAttribute : Attribute { 29 | 30 | /// 31 | /// Specifies the ruleset which should be used when executing this validator. 32 | /// This can be a comma separated list of rulesets. The string "*" can be used to indicate all rulesets. 33 | /// The string "default" can be used to specify those rules not in an explict ruleset. 34 | /// 35 | public string RuleSet { get; set; } 36 | 37 | /// 38 | /// Specifies a whitelist of properties that should be validated, as a comma-separated list. 39 | /// 40 | public string Properties { get; set; } 41 | 42 | /// 43 | /// Specifies an interceptor that can be used to customize the validation process. 44 | /// 45 | public Type Interceptor { get; set; } 46 | 47 | /// 48 | /// Indicates whether this model should skip being validated. The default is false. 49 | /// 50 | public bool Skip { get; set; } 51 | 52 | /// 53 | /// Builds a validator selector from the options specified in the attribute's properties. 54 | /// 55 | /// 56 | public IValidatorSelector ToValidatorSelector(ModelValidationContext mvContext) { 57 | IValidatorSelector selector; 58 | 59 | if (!string.IsNullOrEmpty(RuleSet)) { 60 | var rulesets = RuleSet.Split(',', ';') 61 | .Select(x => x.Trim()) 62 | .ToArray(); 63 | selector = CreateRulesetValidatorSelector(mvContext, rulesets); 64 | } 65 | else if (!string.IsNullOrEmpty(Properties)) { 66 | var properties = Properties.Split(',', ';') 67 | .Select(x => x.Trim()) 68 | .ToArray(); 69 | selector = CreateMemberNameValidatorSelector(mvContext, properties); 70 | } 71 | else { 72 | selector = CreateDefaultValidatorSelector(mvContext); 73 | } 74 | 75 | return selector; 76 | } 77 | 78 | protected virtual IValidatorSelector CreateRulesetValidatorSelector(ModelValidationContext mvContext, string[] ruleSets) { 79 | var cfg = mvContext.ActionContext.HttpContext.RequestServices.GetRequiredService(); 80 | return cfg.ValidatorSelectors.RulesetValidatorSelectorFactory(ruleSets); 81 | } 82 | 83 | protected virtual IValidatorSelector CreateMemberNameValidatorSelector(ModelValidationContext mvContext, string[] properties) { 84 | var cfg = mvContext.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 85 | return cfg.ValidatorSelectors.MemberNameValidatorSelectorFactory(properties); 86 | } 87 | 88 | protected virtual IValidatorSelector CreateDefaultValidatorSelector(ModelValidationContext mvContext) { 89 | var cfg = mvContext.ActionContext.HttpContext.RequestServices.GetValidatorConfiguration(); 90 | return cfg.ValidatorSelectors.DefaultValidatorSelectorFactory(); 91 | } 92 | 93 | public IValidatorInterceptor GetInterceptor() { 94 | if (Interceptor == null) return null; 95 | 96 | if (!typeof(IValidatorInterceptor).IsAssignableFrom(Interceptor)) { 97 | throw new InvalidOperationException("Type {0} is not an IValidatorInterceptor. The Interceptor property of CustomizeValidatorAttribute must implement IValidatorInterceptor."); 98 | } 99 | 100 | var instance = Activator.CreateInstance(Interceptor) as IValidatorInterceptor; 101 | 102 | if (instance == null) { 103 | throw new InvalidOperationException("Type {0} is not an IValidatorInterceptor. The Interceptor property of CustomizeValidatorAttribute must implement IValidatorInterceptor."); 104 | } 105 | 106 | return instance; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1;net5.0;net6.0 4 | 11.3.1 5 | FluentValidation.AspNetCore 6 | FluentValidation.AspNetCore 7 | FluentValidation.AspNetCore 8 | AspNetCore integration for FluentValidation 9 | 10 | FluentValidation 11 is a major release. Please read the upgrade guide at https://docs.fluentvalidation.net/en/latest/upgrading-to-11.html 11 | 12 | Full release notes can be found at https://github.com/FluentValidation/FluentValidation.AspNetCore/releases 13 | 14 | True 15 | False 16 | bin\$(Configuration)\$(TargetFramework)\FluentValidation.AspNetCore.xml 17 | embedded 18 | README.md 19 | Jeremy Skinner 20 | 10 21 | $(NoWarn);1701;1702;1591 22 | true 23 | https://fluentvalidation.net 24 | Copyright (c) Jeremy Skinner, .NET Foundation, and contributors 2008-2022 25 | https://github.com/FluentValidation/FluentValidation.AspNetCore 26 | fluent-validation-icon.png 27 | Apache-2.0 28 | false 29 | en 30 | $(MSBuildProjectDirectory)/../FluentValidation-Release.snk 31 | $(MSBuildProjectDirectory)/../../.build/packages 32 | true 33 | true 34 | true 35 | true 36 | 11.0.0 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidation.AspNetCore.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationBindingMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; 22 | 23 | internal class FluentValidationBindingMetadataProvider : IBindingMetadataProvider { 24 | public const string Prefix = "_FV_REQUIRED|"; 25 | 26 | /// 27 | /// If we're validating a non-nullable value type then 28 | /// MVC will automatically add a "Required" error message. 29 | /// We prefix these messages with a placeholder, so we can identify and remove them 30 | /// during the validation process. 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public void CreateBindingMetadata(BindingMetadataProviderContext context) { 37 | if (context.Key.MetadataKind == ModelMetadataKind.Property) { 38 | var original = context.BindingMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor; 39 | context.BindingMetadata.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(s => Prefix + original(s)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationClientModelValidatorProvider.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using System; 21 | using System.Collections.Generic; 22 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 23 | using System.Linq; 24 | using FluentValidation.Internal; 25 | using FluentValidation.Validators; 26 | using Microsoft.AspNetCore.Http; 27 | using Microsoft.AspNetCore.Mvc.DataAnnotations; 28 | 29 | public delegate IClientModelValidator FluentValidationClientValidatorFactory(ClientValidatorProviderContext context, IValidationRule rule, IRuleComponent component); 30 | 31 | /// 32 | /// Used to generate clientside metadata from FluentValidation's rules. 33 | /// 34 | public class FluentValidationClientModelValidatorProvider : IClientModelValidatorProvider{ 35 | private readonly IHttpContextAccessor _httpContextAccessor; 36 | private readonly ValidatorDescriptorCache _descriptorCache = new(); 37 | 38 | public Dictionary ClientValidatorFactories { get; } = new() { 39 | { typeof(INotNullValidator), (context, rule, component) => new RequiredClientValidator(rule, component) }, 40 | { typeof(INotEmptyValidator), (context, rule, component) => new RequiredClientValidator(rule, component) }, 41 | { typeof(IEmailValidator), (context, rule, component) => new EmailClientValidator(rule, component) }, 42 | { typeof(IRegularExpressionValidator), (context, rule, component) => new RegexClientValidator(rule, component) }, 43 | { typeof(IMaximumLengthValidator), (context, rule, component) => new MaxLengthClientValidator(rule, component) }, 44 | { typeof(IMinimumLengthValidator), (context, rule, component) => new MinLengthClientValidator(rule, component) }, 45 | { typeof(IExactLengthValidator), (context, rule, component) => new StringLengthClientValidator(rule, component)}, 46 | { typeof(ILengthValidator), (context, rule, component) => new StringLengthClientValidator(rule, component)}, 47 | { typeof(IInclusiveBetweenValidator), (context, rule, component) => new RangeClientValidator(rule, component) }, 48 | { typeof(IGreaterThanOrEqualValidator), (context, rule, component) => new RangeMinClientValidator(rule, component) }, 49 | { typeof(ILessThanOrEqualValidator), (context, rule, component) => new RangeMaxClientValidator(rule, component) }, 50 | { typeof(IEqualValidator), (context, rule, component) => new EqualToClientValidator(rule, component) }, 51 | { typeof(ICreditCardValidator), (context, rule, component) => new CreditCardClientValidator(rule, component) }, 52 | }; 53 | 54 | public FluentValidationClientModelValidatorProvider(IHttpContextAccessor httpContextAccessor) { 55 | _httpContextAccessor = httpContextAccessor; 56 | } 57 | 58 | public void Add(Type validatorType, FluentValidationClientValidatorFactory factory) { 59 | if (validatorType == null) throw new ArgumentNullException(nameof(validatorType)); 60 | if (factory == null) throw new ArgumentNullException(nameof(factory)); 61 | 62 | ClientValidatorFactories[validatorType] = factory; 63 | } 64 | 65 | public void CreateValidators(ClientValidatorProviderContext context) { 66 | var descriptor = _descriptorCache.GetCachedDescriptor(context, _httpContextAccessor); 67 | 68 | if (descriptor != null) { 69 | var propertyName = context.ModelMetadata.PropertyName; 70 | 71 | var validatorsWithRules = from rule in descriptor.GetRulesForMember(propertyName) 72 | where !rule.HasCondition && !rule.HasAsyncCondition 73 | let components = rule.Components 74 | where components.Any() 75 | from component in components 76 | where !component.HasCondition && !component.HasAsyncCondition 77 | let modelValidatorForProperty = GetModelValidator(context, rule, component) 78 | where modelValidatorForProperty != null 79 | select modelValidatorForProperty; 80 | 81 | var list = validatorsWithRules.ToList(); 82 | 83 | foreach (var propVal in list) { 84 | context.Results.Add(new ClientValidatorItem { 85 | Validator = propVal, 86 | IsReusable = false 87 | }); 88 | } 89 | 90 | // Must ensure there is at least 1 ClientValidatorItem, set to IsReusable = false 91 | // otherwise MVC will cache the list of validators, assuming there will always be 0 validators for that property 92 | // Which isn't true - we may be using the RulesetForClientsideMessages attribute (or some other mechanism) that can change the client validators that are available 93 | // depending on some context. 94 | if (list.Count == 0) { 95 | context.Results.Add(new ClientValidatorItem {IsReusable = false}); 96 | } 97 | 98 | HandleNonNullableValueTypeRequiredRule(context); 99 | } 100 | } 101 | 102 | // If the property is a non-nullable value type, then MVC will have already generated a Required rule. 103 | // If we've provided our own Required rule, then remove the MVC one. 104 | protected virtual void HandleNonNullableValueTypeRequiredRule(ClientValidatorProviderContext context) { 105 | bool isNonNullableValueType = !TypeAllowsNullValue(context.ModelMetadata.ModelType); 106 | 107 | if (isNonNullableValueType) { 108 | bool fvHasRequiredRule = context.Results.Any(x => x.Validator is RequiredClientValidator); 109 | 110 | if (fvHasRequiredRule) { 111 | var dataAnnotationsRequiredRule = context.Results 112 | .FirstOrDefault(x => x.Validator is RequiredAttributeAdapter); 113 | context.Results.Remove(dataAnnotationsRequiredRule); 114 | } 115 | } 116 | } 117 | 118 | protected virtual IClientModelValidator GetModelValidator(ClientValidatorProviderContext context, IValidationRule rule, IRuleComponent component) { 119 | var type = component.Validator.GetType(); 120 | 121 | var factory = ClientValidatorFactories 122 | .Where(x => x.Key.IsAssignableFrom(type)) 123 | .Select(x => x.Value) 124 | .FirstOrDefault(); 125 | 126 | if (factory != null) { 127 | bool shouldExecute; 128 | var ruleSetToGenerateClientSideRules = RuleSetForClientSideMessagesAttribute.GetRuleSetsForClientValidation(_httpContextAccessor?.HttpContext); 129 | 130 | if (ruleSetToGenerateClientSideRules.Contains(RulesetValidatorSelector.WildcardRuleSetName)) { 131 | // If RuleSet "*" is specified, include all rules. 132 | shouldExecute = true; 133 | } 134 | else { 135 | bool executeDefaultRule = ruleSetToGenerateClientSideRules.Contains(RulesetValidatorSelector.DefaultRuleSetName, StringComparer.OrdinalIgnoreCase) 136 | && (rule.RuleSets == null || rule.RuleSets.Length == 0 || rule.RuleSets.Contains(RulesetValidatorSelector.DefaultRuleSetName, StringComparer.OrdinalIgnoreCase)); 137 | 138 | shouldExecute = (rule.RuleSets != null && ruleSetToGenerateClientSideRules.Intersect(rule.RuleSets, StringComparer.OrdinalIgnoreCase).Any()) || executeDefaultRule; 139 | } 140 | 141 | if (shouldExecute) { 142 | return factory.Invoke(context, rule, component); 143 | } 144 | } 145 | 146 | return null; 147 | } 148 | 149 | private bool TypeAllowsNullValue(Type type) { 150 | return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | namespace FluentValidation.AspNetCore; 22 | 23 | using System; 24 | using System.Collections.Generic; 25 | using System.Linq; 26 | using Internal; 27 | using Microsoft.AspNetCore.Mvc; 28 | using Microsoft.AspNetCore.Mvc.ModelBinding; 29 | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; 30 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 31 | using Microsoft.Extensions.DependencyInjection; 32 | using Results; 33 | 34 | /// 35 | /// ModelValidatorProvider implementation only used for child properties. 36 | /// 37 | public class FluentValidationModelValidatorProvider : IModelValidatorProvider { 38 | private readonly bool _implicitValidationEnabled; 39 | private readonly bool _implicitRootCollectionElementValidationEnabled; 40 | private readonly Func _filter; 41 | 42 | public FluentValidationModelValidatorProvider(bool implicitValidationEnabled) 43 | : this(implicitValidationEnabled, false, default) { 44 | } 45 | 46 | public FluentValidationModelValidatorProvider( 47 | bool implicitValidationEnabled, 48 | bool implicitRootCollectionElementValidationEnabled) 49 | : this(implicitValidationEnabled, implicitRootCollectionElementValidationEnabled, default) { 50 | } 51 | 52 | public FluentValidationModelValidatorProvider( 53 | bool implicitValidationEnabled, 54 | bool implicitRootCollectionElementValidationEnabled, 55 | Func filter) { 56 | _implicitValidationEnabled = implicitValidationEnabled; 57 | _implicitRootCollectionElementValidationEnabled = implicitRootCollectionElementValidationEnabled; 58 | _filter = filter; 59 | } 60 | 61 | public virtual void CreateValidators(ModelValidatorProviderContext context) { 62 | context.Results.Add(new ValidatorItem { 63 | IsReusable = false, 64 | Validator = new FluentValidationModelValidator(_implicitValidationEnabled, _implicitRootCollectionElementValidationEnabled, _filter), 65 | }); 66 | } 67 | } 68 | 69 | /// 70 | /// FluentValidation's implementation of an ASP.NET Core model validator. 71 | /// 72 | public class FluentValidationModelValidator : IModelValidator { 73 | private readonly bool _implicitValidationEnabled; 74 | private readonly bool _implicitRootCollectionElementValidationEnabled; 75 | private readonly Func _filter; 76 | 77 | public FluentValidationModelValidator(bool implicitValidationEnabled) 78 | : this(implicitValidationEnabled, false, default) { 79 | } 80 | 81 | public FluentValidationModelValidator( 82 | bool implicitValidationEnabled, 83 | bool implicitRootCollectionElementValidationEnabled) 84 | : this(implicitValidationEnabled, implicitRootCollectionElementValidationEnabled, default) { 85 | } 86 | 87 | public FluentValidationModelValidator( 88 | bool implicitValidationEnabled, 89 | bool implicitRootCollectionElementValidationEnabled, 90 | Func filter) { 91 | _implicitValidationEnabled = implicitValidationEnabled; 92 | _implicitRootCollectionElementValidationEnabled = implicitRootCollectionElementValidationEnabled; 93 | _filter = filter; 94 | } 95 | 96 | public virtual IEnumerable Validate(ModelValidationContext mvContext) { 97 | if (ShouldSkip(mvContext)) { 98 | return Enumerable.Empty(); 99 | } 100 | 101 | IValidator validator; 102 | 103 | #pragma warning disable CS0618 104 | var factory = mvContext.ActionContext.HttpContext.RequestServices.GetService(); 105 | #pragma warning restore CS0618 106 | 107 | if (factory != null) { 108 | validator = factory?.GetValidator(mvContext.ModelMetadata.ModelType); 109 | } 110 | else { 111 | validator = mvContext.ActionContext.HttpContext.RequestServices.GetService(mvContext.ModelMetadata.ModelType) as IValidator; 112 | } 113 | 114 | 115 | if (validator != null) { 116 | var customizations = GetCustomizations(mvContext.ActionContext, mvContext.Model); 117 | 118 | if (customizations.Skip) { 119 | return Enumerable.Empty(); 120 | } 121 | 122 | if (mvContext.Container != null) { 123 | var containerCustomizations = GetCustomizations(mvContext.ActionContext, mvContext.Container); 124 | if (containerCustomizations.Skip) { 125 | return Enumerable.Empty(); 126 | } 127 | } 128 | 129 | var selector = customizations.ToValidatorSelector(mvContext); 130 | var interceptor = customizations.GetInterceptor() 131 | ?? validator as IValidatorInterceptor 132 | ?? mvContext.ActionContext.HttpContext.RequestServices.GetService(); 133 | 134 | IValidationContext context = new ValidationContext(mvContext.Model, new PropertyChain(), selector); 135 | context.RootContextData["InvokedByMvc"] = true; 136 | 137 | // For backwards compatibility, store the service provider in the validation context. 138 | // This approach works with both FluentValidation.DependencyInjectionExtensions 11.x 139 | // and FluentValidation.DependencyInjectionExtensions 12.x. 140 | // Do not use context.SetServiceProvider extension method as this no longer 141 | // exists in 12.x. 142 | context.RootContextData["_FV_ServiceProvider"] = mvContext.ActionContext.HttpContext.RequestServices; 143 | 144 | if (interceptor != null) { 145 | // Allow the user to provide a customized context 146 | // However, if they return null then just use the original context. 147 | context = interceptor.BeforeAspNetValidation(mvContext.ActionContext, context) ?? context; 148 | } 149 | 150 | var result = validator.Validate(context); 151 | 152 | if (interceptor != null) { 153 | // allow the user to provide a custom collection of failures, which could be empty. 154 | // However, if they return null then use the original collection of failures. 155 | result = interceptor.AfterAspNetValidation(mvContext.ActionContext, context, result) ?? result; 156 | } 157 | 158 | return result.Errors.Select(x => new ModelValidationResult(x.PropertyName, x.ErrorMessage)); 159 | } 160 | 161 | return Enumerable.Empty(); 162 | } 163 | 164 | protected bool ShouldSkip(ModelValidationContext mvContext) { 165 | //Apply custom filter (if specified) 166 | //validation will be skipped unless we match on this filter 167 | if (_filter != null && !_filter.Invoke(mvContext.ModelMetadata.ModelType)) { 168 | return true; 169 | } 170 | 171 | // Skip if there's nothing to process. 172 | if (mvContext.Model == null) { 173 | return true; 174 | } 175 | 176 | // If implicit validation is disabled, then we want to only validate the root object. 177 | if (!_implicitValidationEnabled) { 178 | var rootMetadata = GetRootMetadata(mvContext); 179 | 180 | // We should always have root metadata, so this should never happen... 181 | if (rootMetadata == null) return true; 182 | 183 | var modelMetadata = mvContext.ModelMetadata; 184 | 185 | // Careful when handling properties. 186 | // If we're processing a property of our root object, 187 | // then we always skip if implicit validation is disabled 188 | // However if our root object *is* a property (because of [BindProperty]) 189 | // then this is OK to proceed. 190 | if (modelMetadata.MetadataKind == ModelMetadataKind.Property) { 191 | if (!ReferenceEquals(rootMetadata, modelMetadata)) { 192 | // The metadata for the current property is not the same as the root metadata 193 | // This means we're validating a property on a model, so we want to skip. 194 | return true; 195 | } 196 | } 197 | 198 | // If we're handling a type, we need to make sure we're handling the root type. 199 | // When MVC encounters child properties, it will set the MetadataKind to Type, 200 | // so we can't use the MetadataKind to differentiate the root from the child property. 201 | // Instead check if our cached root metadata is the same. 202 | // If they're not, then it means we're handling a child property, so we should skip 203 | // validation if implicit validation is disabled 204 | else if (modelMetadata.MetadataKind == ModelMetadataKind.Type) { 205 | // If implicit validation of root collection elements is enabled then we 206 | // do want to validate the type if it matches the element type of the root collection 207 | if (_implicitRootCollectionElementValidationEnabled && IsRootCollectionElementType(rootMetadata, modelMetadata.ModelType)) { 208 | return false; 209 | } 210 | 211 | if (!ReferenceEquals(rootMetadata, modelMetadata)) { 212 | // The metadata for the current type is not the same as the root metadata 213 | // This means we're validating a child element of a collection or sub property. 214 | // Skip it as implicit validation is disabled. 215 | return true; 216 | } 217 | } 218 | else if (modelMetadata.MetadataKind == ModelMetadataKind.Parameter) { 219 | // If we're working with record types then metadata kind will always be parameter. 220 | if (!ReferenceEquals(rootMetadata, modelMetadata)) { 221 | return true; 222 | } 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | /// 230 | /// Gets the metadata object for the root object being validated. 231 | /// 232 | /// MVC Validation context 233 | /// Metadata instance. 234 | protected static ModelMetadata GetRootMetadata(ModelValidationContext mvContext) { 235 | return MvcValidationHelper.GetRootMetadata(mvContext); 236 | } 237 | 238 | /// 239 | /// Gets customizations associated with this validation request. 240 | /// 241 | /// Current action context 242 | /// The object being validated 243 | /// Customizations 244 | protected static CustomizeValidatorAttribute GetCustomizations(ActionContext context, object model) { 245 | return MvcValidationHelper.GetCustomizations(context, model); 246 | } 247 | 248 | private static bool IsRootCollectionElementType(ModelMetadata rootMetadata, Type modelType) { 249 | if (!rootMetadata.IsEnumerableType) 250 | return false; 251 | 252 | return modelType == rootMetadata.ElementType; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Reflection; 24 | using Microsoft.Extensions.DependencyInjection; 25 | 26 | /// 27 | /// Auto-validation configuration. 28 | /// 29 | public class FluentValidationAutoValidationConfiguration { 30 | 31 | /// 32 | /// Whether or not child properties should be implicitly validated if a matching validator can be found. By default this is false, and you should wire up child validators using SetValidator. 33 | /// 34 | [Obsolete("Implicit validation of child properties deprecated and will be removed in a future release. Please use SetValidator instead. For details see https://github.com/FluentValidation/FluentValidation/issues/1960")] 35 | public bool ImplicitlyValidateChildProperties { get; set; } 36 | 37 | /// 38 | /// Gets or sets a value indicating whether the elements of a root model should be implicitly validated when 39 | /// the root model is a collection type and a matching validator can be found for the element type. 40 | /// By default this is , and you will need to create a validator for the collection type 41 | /// (unless is . 42 | /// 43 | [Obsolete("Implicit validation of root collection elements is deprecated and will be removed in a future release. Please use an explicit collection validator instead. For details see https://github.com/FluentValidation/FluentValidation/issues/1960")] 44 | public bool ImplicitlyValidateRootCollectionElements { get; set; } 45 | 46 | /// 47 | /// The type of validator factory to use. Uses the ServiceProviderValidatorFactory by default. 48 | /// 49 | [Obsolete("IValidatorFactory and its implementors are deprecated. Please use the Service Provider directly. For details see https://github.com/FluentValidation/FluentValidation/issues/1961")] 50 | public Type ValidatorFactoryType { get; set; } 51 | 52 | /// 53 | /// The validator factory to use. Uses the ServiceProviderValidatorFactory by default. 54 | /// 55 | [Obsolete("IValidatorFactory and its implementors are deprecated. Please use the Service Provider directly. For details see https://github.com/FluentValidation/FluentValidation/issues/1961")] 56 | public IValidatorFactory ValidatorFactory { get; set; } 57 | 58 | /// 59 | /// By default Data Annotations validation will also run as well as FluentValidation. 60 | /// Setting this to true will disable DataAnnotations and only run FluentValidation. 61 | /// 62 | public bool DisableDataAnnotationsValidation { get; set; } 63 | 64 | 65 | /// 66 | /// When specified, automatic validation will only apply to the types matched by the filter. 67 | /// If the filter does not match, automatic validation will not be applied. This can be useful 68 | /// for specific situations where you want to opt in/out of automatic validation 69 | /// Example: Filter = type => type == typeof(Model) 70 | /// 71 | public Func Filter { get; set; } 72 | } 73 | 74 | /// 75 | /// FluentValidation asp.net core configuration 76 | /// 77 | public class FluentValidationMvcConfiguration : FluentValidationAutoValidationConfiguration { 78 | private readonly IServiceCollection _services; 79 | 80 | [Obsolete] 81 | public FluentValidationMvcConfiguration(ValidatorConfiguration validatorOptions) { 82 | #pragma warning disable CS0618 83 | ValidatorOptions = validatorOptions; 84 | #pragma warning restore CS0618 85 | } 86 | 87 | internal FluentValidationMvcConfiguration(ValidatorConfiguration validatorOptions, IServiceCollection services) { 88 | _services = services; 89 | #pragma warning disable CS0618 90 | ValidatorOptions = validatorOptions; 91 | #pragma warning restore CS0618 92 | } 93 | 94 | /// 95 | /// Options that are used to configure all validators. 96 | /// 97 | [Obsolete("Global options should be set using the static ValidatorOptions.Global instead.")] 98 | public ValidatorConfiguration ValidatorOptions { get; private set; } 99 | 100 | /// 101 | /// Enables or disables localization support within FluentValidation 102 | /// 103 | [Obsolete("Set the static ValidatorOptions.Global.LanguageManager.Enabled property instead.")] 104 | public bool LocalizationEnabled { 105 | get => ValidatorOptions.LanguageManager.Enabled; 106 | set => ValidatorOptions.LanguageManager.Enabled = value; 107 | } 108 | 109 | internal bool ClientsideEnabled = true; 110 | internal Action ClientsideConfig = x => {}; 111 | 112 | /// 113 | /// Whether automatic server-side validation should be enabled (default true). 114 | /// 115 | public bool AutomaticValidationEnabled { get; set; } = true; 116 | 117 | /// 118 | /// Registers all validators derived from AbstractValidator within the assembly containing the specified type 119 | /// 120 | /// Optional filter that allows certain types to be skipped from registration. 121 | /// The service lifetime that should be used for the validator registration. Defaults to Scoped 122 | /// Include internal validators. The default is false. 123 | [Obsolete("RegisterValidatorsFromAssemblyContaining is deprecated. Call services.AddValidatorsFromAssemblyContaining instead, which has the same effect. See https://github.com/FluentValidation/FluentValidation/issues/1963")] 124 | public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining(Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Scoped, bool includeInternalTypes = false) { 125 | return RegisterValidatorsFromAssemblyContaining(typeof(T), filter, lifetime, includeInternalTypes); 126 | } 127 | 128 | /// 129 | /// Registers all validators derived from AbstractValidator within the assembly containing the specified type 130 | /// 131 | /// The type that indicates which assembly that should be scanned 132 | /// Optional filter that allows certain types to be skipped from registration. 133 | /// The service lifetime that should be used for the validator registration. Defaults to Scoped 134 | /// Include internal validators. The default is false. 135 | [Obsolete("RegisterValidatorsFromAssemblyContaining is deprecated. Call services.AddValidatorsFromAssemblyContaining instead, which has the same effect. See https://github.com/FluentValidation/FluentValidation/issues/1963")] 136 | public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblyContaining(Type type, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Scoped, bool includeInternalTypes = false) { 137 | return RegisterValidatorsFromAssembly(type.Assembly, filter, lifetime, includeInternalTypes); 138 | } 139 | 140 | /// 141 | /// Registers all validators derived from AbstractValidator within the specified assembly 142 | /// 143 | /// The assembly to scan 144 | /// Optional filter that allows certain types to be skipped from registration. 145 | /// The service lifetime that should be used for the validator registration. Defaults to Scoped 146 | /// Include internal validators. The default is false. 147 | [Obsolete("RegisterValidatorsFromAssembly is deprecated. Call services.AddValidatorsFromAssembly instead, which has the same effect. See https://github.com/FluentValidation/FluentValidation/issues/1963")] 148 | public FluentValidationMvcConfiguration RegisterValidatorsFromAssembly(Assembly assembly, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Scoped, bool includeInternalTypes = false) { 149 | _services.AddValidatorsFromAssembly(assembly, lifetime, filter, includeInternalTypes); 150 | 151 | #pragma warning disable CS0618 152 | ValidatorFactoryType = typeof(ServiceProviderValidatorFactory); 153 | #pragma warning restore CS0618 154 | return this; 155 | } 156 | 157 | /// 158 | /// Registers all validators derived from AbstractValidator within the specified assemblies 159 | /// 160 | /// The assemblies to scan 161 | /// Optional filter that allows certain types to be skipped from registration. 162 | /// The service lifetime that should be used for the validator registration. Defaults to Scoped 163 | /// Include internal validators. The default is false. 164 | [Obsolete("RegisterValidatorsFromAssemblies is deprecated. Call services.AddValidatorsFromAssemblies instead, which has the same effect. See https://github.com/FluentValidation/FluentValidation/issues/1963")] 165 | public FluentValidationMvcConfiguration RegisterValidatorsFromAssemblies(IEnumerable assemblies, Func filter = null, ServiceLifetime lifetime = ServiceLifetime.Scoped, bool includeInternalTypes = false) { 166 | _services.AddValidatorsFromAssemblies(assemblies, lifetime, filter, includeInternalTypes); 167 | 168 | #pragma warning disable CS0618 169 | ValidatorFactoryType = typeof(ServiceProviderValidatorFactory); 170 | #pragma warning restore CS0618 171 | return this; 172 | } 173 | 174 | /// 175 | /// Configures clientside validation support 176 | /// 177 | /// 178 | /// Whether clientside validation integration is enabled 179 | /// 180 | [Obsolete("ConfigureClientsideValidation is deprecated and will be removed in a future release. To configure client-side validation call services.AddFluentValidationClientsideAdapters(config => ...) instead. For details see https://github.com/FluentValidation/FluentValidation/issues/1965")] 181 | public FluentValidationMvcConfiguration ConfigureClientsideValidation(Action clientsideConfig=null, bool enabled=true) { 182 | if (clientsideConfig != null) { 183 | ClientsideConfig = clientsideConfig; 184 | } 185 | ClientsideEnabled = enabled; 186 | return this; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System; 22 | using Microsoft.AspNetCore.Mvc; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding; 24 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.Extensions.Options; 27 | using FluentValidation; 28 | using System.Linq; 29 | using Microsoft.AspNetCore.Http; 30 | using Microsoft.Extensions.DependencyInjection.Extensions; 31 | 32 | public static class FluentValidationMvcExtensions { 33 | /// 34 | /// Adds Fluent Validation services to the specified 35 | /// . 36 | /// 37 | /// 38 | /// An that can be used to further configure the 39 | /// MVC services. 40 | /// 41 | /// 42 | /// Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see . 43 | /// 44 | [Obsolete("Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see https://github.com/FluentValidation/FluentValidation/issues/1965")] 45 | public static IMvcCoreBuilder AddFluentValidation(this IMvcCoreBuilder mvcBuilder, Action configurationExpression = null) { 46 | mvcBuilder.Services.AddFluentValidation(configurationExpression); 47 | return mvcBuilder; 48 | } 49 | 50 | /// 51 | /// Adds Fluent Validation services to the specified 52 | /// . 53 | /// 54 | /// 55 | /// An that can be used to further configure the 56 | /// MVC services. 57 | /// 58 | /// 59 | /// Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see . 60 | /// 61 | [Obsolete("Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see https://github.com/FluentValidation/FluentValidation/issues/1965")] 62 | public static IMvcBuilder AddFluentValidation(this IMvcBuilder mvcBuilder, Action configurationExpression = null) { 63 | mvcBuilder.Services.AddFluentValidation(configurationExpression); 64 | return mvcBuilder; 65 | } 66 | 67 | #pragma warning disable CS0618 68 | /// 69 | /// Adds Fluent Validation services to the specified 70 | /// . 71 | /// 72 | /// 73 | /// A reference to this instance after the operation has completed. 74 | /// 75 | /// 76 | /// Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see . 77 | /// 78 | [Obsolete("Calling AddFluentValidation() is deprecated. Call services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters() instead, which has the same effect. For details see https://github.com/FluentValidation/FluentValidation/issues/1965")] 79 | public static IServiceCollection AddFluentValidation(this IServiceCollection services, Action configurationExpression = null) { 80 | var config = new FluentValidationMvcConfiguration(ValidatorOptions.Global, services); 81 | configurationExpression?.Invoke(config); 82 | 83 | services.AddSingleton(config.ValidatorOptions); 84 | 85 | if (config.AutomaticValidationEnabled) { 86 | services.AddFluentValidationAutoValidation(cfg => { 87 | cfg.DisableDataAnnotationsValidation = config.DisableDataAnnotationsValidation; 88 | cfg.ImplicitlyValidateChildProperties = config.ImplicitlyValidateChildProperties; 89 | cfg.ImplicitlyValidateRootCollectionElements = config.ImplicitlyValidateRootCollectionElements; 90 | cfg.ValidatorFactory = config.ValidatorFactory; 91 | cfg.ValidatorFactoryType = config.ValidatorFactoryType; 92 | }); 93 | } 94 | 95 | if (config.ClientsideEnabled) { 96 | services.AddFluentValidationClientsideAdapters(config.ClientsideConfig); 97 | } 98 | 99 | return services; 100 | } 101 | 102 | /// 103 | /// Enables integration between FluentValidation and ASP.NET MVC's automatic validation pipeline. 104 | /// 105 | /// services 106 | /// Configuration callback 107 | /// The service collection 108 | public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection services, Action configurationExpression = null) { 109 | var config = new FluentValidationAutoValidationConfiguration(); 110 | configurationExpression?.Invoke(config); 111 | 112 | services.TryAddSingleton(ValidatorOptions.Global); 113 | 114 | if (config.ValidatorFactory != null) { 115 | // Allow user to register their own IValidatorFactory instance, before falling back to try resolving by Type. 116 | var factory = config.ValidatorFactory; 117 | services.Add(ServiceDescriptor.Scoped(s => factory)); 118 | } 119 | else { 120 | services.Add(ServiceDescriptor.Scoped(typeof(IValidatorFactory), config.ValidatorFactoryType ?? typeof(ServiceProviderValidatorFactory))); 121 | } 122 | 123 | services.Add(ServiceDescriptor.Singleton(s => { 124 | var options = s.GetRequiredService>().Value; 125 | var metadataProvider = s.GetRequiredService(); 126 | return new FluentValidationObjectModelValidator(metadataProvider, options.ModelValidatorProviders, !config.DisableDataAnnotationsValidation); 127 | })); 128 | 129 | services.Configure(options => { 130 | // Check if the providers have already been added. 131 | // We shouldn't have to do this, but there's a bug in the ASP.NET Core integration 132 | // testing components that can cause Configureservices to be called multple times 133 | // meaning we end up with duplicates. 134 | 135 | if (!options.ModelMetadataDetailsProviders.Any(x => x is FluentValidationBindingMetadataProvider)) { 136 | options.ModelMetadataDetailsProviders.Add(new FluentValidationBindingMetadataProvider()); 137 | } 138 | 139 | if (!options.ModelValidatorProviders.Any(x => x is FluentValidationModelValidatorProvider)) { 140 | options.ModelValidatorProviders.Insert(0, new FluentValidationModelValidatorProvider( 141 | config.ImplicitlyValidateChildProperties, 142 | config.ImplicitlyValidateRootCollectionElements, 143 | config.Filter)); 144 | } 145 | }); 146 | 147 | return services; 148 | } 149 | 150 | #pragma warning restore CS0618 151 | 152 | /// 153 | /// Enables integration between FluentValidation and ASP.NET client-side validation. See for details. 154 | /// 155 | /// Service collection 156 | /// Configuration expression 157 | /// 158 | public static IServiceCollection AddFluentValidationClientsideAdapters(this IServiceCollection services, Action configuration = null) { 159 | services.AddHttpContextAccessor(); 160 | services.TryAddSingleton(ValidatorOptions.Global); 161 | 162 | #pragma warning disable CS0618 163 | services.TryAddScoped(); 164 | #pragma warning restore CS0618 165 | 166 | services.TryAddEnumerable(ServiceDescriptor.Singleton, FluentValidationViewOptionsSetup>(s => { 167 | return new FluentValidationViewOptionsSetup(configuration, s.GetService()); 168 | })); 169 | 170 | return services; 171 | } 172 | } 173 | 174 | internal class FluentValidationViewOptionsSetup : IConfigureOptions { 175 | private readonly Action _action; 176 | private readonly IHttpContextAccessor _httpContextAccessor; 177 | 178 | public FluentValidationViewOptionsSetup(Action action, IHttpContextAccessor httpContextAccessor) { 179 | _action = action; 180 | _httpContextAccessor = httpContextAccessor; 181 | } 182 | 183 | public void Configure(MvcViewOptions options) { 184 | var provider = new FluentValidationClientModelValidatorProvider(_httpContextAccessor); 185 | _action?.Invoke(provider); 186 | options.ClientModelValidatorProviders.Add(provider); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationObjectModelValidator.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System.Collections.Generic; 22 | using System.Linq; 23 | using Microsoft.AspNetCore.Mvc; 24 | using Microsoft.AspNetCore.Mvc.ModelBinding; 25 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 26 | 27 | internal class FluentValidationObjectModelValidator : ObjectModelValidator { 28 | private readonly bool _runMvcValidation; 29 | private readonly FluentValidationModelValidatorProvider _fvProvider; 30 | 31 | public FluentValidationObjectModelValidator( 32 | IModelMetadataProvider modelMetadataProvider, 33 | IList validatorProviders, bool runMvcValidation) 34 | : base(modelMetadataProvider, validatorProviders) { 35 | _runMvcValidation = runMvcValidation; 36 | _fvProvider = validatorProviders.SingleOrDefault(x => x is FluentValidationModelValidatorProvider) as FluentValidationModelValidatorProvider; 37 | } 38 | 39 | public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) { 40 | // Setting as to whether we should run only FV or FV + the other validator providers 41 | var validatorProviderToUse = _runMvcValidation ? validatorProvider : _fvProvider; 42 | 43 | var visitor = new FluentValidationVisitor( 44 | actionContext, 45 | validatorProviderToUse, 46 | validatorCache, 47 | metadataProvider, 48 | validationState); 49 | 50 | return visitor; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/FluentValidationVisitor.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System; 22 | using Microsoft.AspNetCore.Mvc; 23 | using Microsoft.AspNetCore.Mvc.ModelBinding; 24 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 25 | using static MvcValidationHelper; 26 | 27 | internal class FluentValidationVisitor : ValidationVisitor { 28 | public FluentValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) 29 | : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { 30 | ValidateComplexTypesIfChildValidationFails = true; 31 | } 32 | 33 | // This overload needs to be in place for both .NET 5 and .NET Core 2/3 34 | public override bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel) { 35 | bool BaseValidate() 36 | => base.Validate(metadata, key, model, alwaysValidateAtTopLevel); 37 | 38 | return ValidateInternal(metadata, key, model, BaseValidate); 39 | } 40 | 41 | #if !NETCOREAPP3_1 42 | // .NET 5+ has this additional overload as an entry point. 43 | public override bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel, object container) { 44 | bool BaseValidate() 45 | => base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); 46 | 47 | return ValidateInternal(metadata, key, model, BaseValidate); 48 | } 49 | #endif 50 | 51 | private bool ValidateInternal(ModelMetadata metadata, string key, object model, Func continuation) { 52 | SetRootMetadata(Context, metadata); 53 | 54 | // Store and remove any implicit required messages. 55 | // Later we'll re-add those that are still relevant. 56 | var requiredErrorsNotHandledByFv = RemoveImplicitRequiredErrors(Context); 57 | 58 | // Apply any customizations made with the CustomizeValidatorAttribute 59 | if (model != null) { 60 | CacheCustomizations(Context, model, key); 61 | } 62 | 63 | var result = continuation(); 64 | 65 | // Re-add errors that we took out if FV didn't add a key. 66 | ReApplyUnhandledImplicitRequiredErrors(requiredErrorsNotHandledByFv); 67 | 68 | // Remove duplicates. This can happen if someone has implicit child validation turned on and also adds an explicit child validator. 69 | RemoveDuplicateModelstateEntries(Context); 70 | 71 | return result; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/IValidatorInterceptor.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.AspNetCore; 19 | 20 | using FluentValidation.Results; 21 | using Microsoft.AspNetCore.Mvc; 22 | using FluentValidation; 23 | 24 | /// 25 | /// Specifies an interceptor that can be used to provide hooks that will be called before and after MVC validation occurs. 26 | /// 27 | public interface IValidatorInterceptor { 28 | /// 29 | /// Invoked before MVC validation takes place which allows the ValidationContext to be customized prior to validation. 30 | /// It should return a ValidationContext object. 31 | /// 32 | /// Action Context 33 | /// Validation Context 34 | /// Validation Context 35 | IValidationContext BeforeAspNetValidation(ActionContext actionContext, IValidationContext commonContext); 36 | 37 | /// 38 | /// Invoked after MVC validation takes place which allows the result to be customized. 39 | /// It should return a ValidationResult. 40 | /// 41 | /// Controller Context 42 | /// Validation Context 43 | /// The result of validation. 44 | /// Validation Context 45 | ValidationResult AfterAspNetValidation(ActionContext actionContext, IValidationContext validationContext, ValidationResult result); 46 | } 47 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/MvcValidationHelper.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | 22 | namespace FluentValidation.AspNetCore; 23 | 24 | using System; 25 | using System.Collections.Generic; 26 | using System.Linq; 27 | using Microsoft.AspNetCore.Mvc; 28 | using Microsoft.AspNetCore.Mvc.Abstractions; 29 | using Microsoft.AspNetCore.Mvc.Controllers; 30 | using Microsoft.AspNetCore.Mvc.ModelBinding; 31 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 32 | using Microsoft.AspNetCore.Mvc.RazorPages; 33 | using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; 34 | using Microsoft.Extensions.DependencyInjection; 35 | 36 | /// 37 | /// Utilities for working around limitations of the MVC validation api. 38 | /// Used by 39 | /// 40 | internal static class MvcValidationHelper { 41 | internal static void SetRootMetadata(ActionContext context, ModelMetadata metadata) { 42 | context.HttpContext.Items["_FV_ROOT_METADATA"] = metadata; 43 | } 44 | 45 | internal static ModelMetadata GetRootMetadata(ModelValidationContext context) { 46 | if (context.ActionContext.HttpContext.Items 47 | .TryGetValue("_FV_ROOT_METADATA", out var rootMetadata)) { 48 | return rootMetadata as ModelMetadata; 49 | } 50 | 51 | return null; 52 | } 53 | 54 | internal static List> RemoveImplicitRequiredErrors(ActionContext actionContext) { 55 | // This is all to work around the default "Required" messages. 56 | var requiredErrorsNotHandledByFv = new List>(); 57 | 58 | foreach (KeyValuePair entry in actionContext.ModelState) { 59 | List errorsToModify = new List(); 60 | 61 | if (entry.Value.ValidationState == ModelValidationState.Invalid) { 62 | foreach (var err in entry.Value.Errors) { 63 | if (err.ErrorMessage.StartsWith(FluentValidationBindingMetadataProvider.Prefix)) { 64 | errorsToModify.Add(err); 65 | } 66 | } 67 | 68 | foreach (ModelError err in errorsToModify) { 69 | entry.Value.Errors.Clear(); 70 | entry.Value.ValidationState = ModelValidationState.Unvalidated; 71 | requiredErrorsNotHandledByFv.Add(new KeyValuePair(entry.Value, new ModelError(err.ErrorMessage.Replace(FluentValidationBindingMetadataProvider.Prefix, string.Empty)))); 72 | } 73 | } 74 | } 75 | 76 | return requiredErrorsNotHandledByFv; 77 | } 78 | 79 | internal static CustomizeValidatorAttribute GetCustomizations(ActionContext actionContext, Type type, string prefix) { 80 | 81 | IList FilterParameterDescriptors(IList parameters) { 82 | return parameters 83 | .Where(x => x.ParameterType == type) 84 | .Where(x => (x.BindingInfo != null && x.BindingInfo.BinderModelName != null && x.BindingInfo.BinderModelName == prefix) || x.Name == prefix || (prefix == string.Empty && x.BindingInfo?.BinderModelName == null)) 85 | .OfType() 86 | .ToList(); 87 | } 88 | 89 | CustomizeValidatorAttribute attribute = null; 90 | 91 | if (actionContext is ControllerContext controllerContext && controllerContext.ActionDescriptor?.Parameters != null) { 92 | 93 | var descriptors = FilterParameterDescriptors(actionContext.ActionDescriptor.Parameters); 94 | 95 | if (descriptors.Count == 1) { 96 | attribute = descriptors[0].ParameterInfo.GetCustomAttributes(typeof(CustomizeValidatorAttribute), true).FirstOrDefault() as CustomizeValidatorAttribute; 97 | } 98 | } 99 | else if (actionContext is PageContext pageContext && pageContext.ActionDescriptor?.BoundProperties != null) { 100 | 101 | var descriptors = FilterParameterDescriptors(pageContext.ActionDescriptor.BoundProperties); 102 | 103 | if (descriptors.Count == 1) { 104 | attribute = descriptors[0].Property.GetCustomAttributes(typeof(CustomizeValidatorAttribute), true).FirstOrDefault() as CustomizeValidatorAttribute; 105 | } 106 | } 107 | 108 | return attribute ?? new CustomizeValidatorAttribute(); 109 | } 110 | 111 | internal static void CacheCustomizations(ActionContext context, object model, string key) { 112 | var customizations = GetCustomizations(context, model.GetType(), key); 113 | context.HttpContext.Items["_FV_Customizations"] = (model, customizations); 114 | } 115 | 116 | internal static void ReApplyUnhandledImplicitRequiredErrors(List> requiredErrorsNotHandledByFv) { 117 | foreach (var pair in requiredErrorsNotHandledByFv) { 118 | if (pair.Key.ValidationState != ModelValidationState.Invalid) { 119 | pair.Key.Errors.Add(pair.Value); 120 | pair.Key.ValidationState = ModelValidationState.Invalid; 121 | } 122 | } 123 | } 124 | 125 | internal static void RemoveDuplicateModelstateEntries(ActionContext actionContext) { 126 | foreach (var entry in actionContext.ModelState) { 127 | if (entry.Value.ValidationState == ModelValidationState.Invalid) { 128 | var existing = new HashSet(); 129 | 130 | foreach (var err in entry.Value.Errors.ToList()) { 131 | //ToList to create a copy so we can remove from the original 132 | if (existing.Contains(err.ErrorMessage)) { 133 | entry.Value.Errors.Remove(err); 134 | } 135 | else { 136 | existing.Add(err.ErrorMessage); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | internal static CustomizeValidatorAttribute GetCustomizations(ActionContext ctx, object model) { 144 | if (ctx.HttpContext.Items["_FV_Customizations"] is ValueTuple customizations 145 | && ReferenceEquals(model, customizations.Item1)) { 146 | return customizations.Item2; // the attribute 147 | } 148 | return new CustomizeValidatorAttribute(); 149 | } 150 | 151 | internal static ValidatorConfiguration GetValidatorConfiguration(this IServiceProvider serviceProvider) 152 | => serviceProvider.GetRequiredService(); 153 | } 154 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/README.md: -------------------------------------------------------------------------------- 1 | The `FluentValidation.AspNetCore` package extends FluentValidation to enable automatic validation within ASP.NET Core Controllers. 2 | 3 | Instructions for using this package can be found [on the github project page](https://github.com/FluentValidation/FluentValidation.AspNetCore#aspnet-core-integration-for-fluentvalidation). 4 | 5 | ### Supporting the project 6 | 7 | If you use FluentValidation in a commercial project, 8 | please sponsor the project financially. 9 | FluentValidation is developed and supported by [@JeremySkinner](https://github.com/JeremySkinner) 10 | for free in his spare time and financial sponsorship helps keep the project going. 11 | You can sponsor the project via either [GitHub sponsors](https://github.com/sponsors/JeremySkinner) or [OpenCollective](https://opencollective.com/FluentValidation). 12 | 13 | ### Full Documentation 14 | 15 | Documentation for using FluentValidation.AspNetCore can be found [on the github project page](https://github.com/FluentValidation/FluentValidation.AspNetCore#aspnet-core-integration-for-fluentvalidation). 16 | 17 | Documentation for the main FluentValidation package can be found at 18 | [https://docs.fluentvalidation.net](https://docs.fluentvalidation.net) 19 | 20 | ### Release Notes and Change Log 21 | 22 | Release notes [can be found on GitHub](https://github.com/FluentValidation/FluentValidation.AspNetCore/releases). 23 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/RuleSetForClientSideMessagesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.AspNetCore; 2 | 3 | using System; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | /// 8 | /// Specifies which ruleset should be used when deciding which validators should be used to generate client-side messages. 9 | /// 10 | public class RuleSetForClientSideMessagesAttribute : ActionFilterAttribute { 11 | 12 | private readonly string[] _ruleSets; 13 | 14 | public RuleSetForClientSideMessagesAttribute(string ruleSet) => _ruleSets = new[] { ruleSet }; 15 | 16 | public RuleSetForClientSideMessagesAttribute(params string[] ruleSets) => _ruleSets = ruleSets; 17 | 18 | public override void OnResultExecuting(ResultExecutingContext context) { 19 | var contextAccessor = context.HttpContext.RequestServices.GetService(typeof(IHttpContextAccessor)); 20 | 21 | if (contextAccessor == null) { 22 | throw new InvalidOperationException("Cannot use the RuleSetForClientSideMessagesAttribute unless the IHttpContextAccessor is registered with the service provider. Make sure the provider is registered by calling services.AddSingleton(); in your Startup class's ConfigureServices method"); 23 | } 24 | 25 | context.HttpContext.SetRulesetForClientsideMessages(_ruleSets); 26 | } 27 | 28 | /// 29 | /// Allows the ruleset used for generating clientside metadata to be overriden. 30 | /// By default, only rules not in a ruleset will be used. 31 | /// 32 | /// 33 | /// 34 | public static void SetRulesetForClientValidation(HttpContext context, string[] ruleSets) => context.SetRulesetForClientsideMessages(ruleSets); 35 | 36 | /// 37 | /// Gets the rulesets used to generate clientside validation metadata. 38 | /// 39 | /// 40 | /// 41 | public static string[] GetRuleSetsForClientValidation(HttpContext context) => context.GetRuleSetsForClientValidation(); 42 | } 43 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/ValidationResultExtensions.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | namespace FluentValidation.AspNetCore; 22 | 23 | using FluentValidation.Internal; 24 | using FluentValidation.Results; 25 | using Microsoft.AspNetCore.Http; 26 | using Microsoft.AspNetCore.Mvc; 27 | using Microsoft.AspNetCore.Mvc.ModelBinding; 28 | using Microsoft.AspNetCore.Mvc.RazorPages; 29 | 30 | public static class ValidationResultExtension { 31 | 32 | private const string _rulesetKey = "_FV_ClientSideRuleSet"; 33 | 34 | /// 35 | /// Stores the errors in a ValidationResult object to the specified modelstate dictionary. 36 | /// 37 | /// The validation result to store 38 | /// The ModelStateDictionary to store the errors in. 39 | public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState) { 40 | if (!result.IsValid) { 41 | foreach (var error in result.Errors) { 42 | modelState.AddModelError(error.PropertyName, error.ErrorMessage); 43 | } 44 | } 45 | } 46 | 47 | /// 48 | /// Stores the errors in a ValidationResult object to the specified modelstate dictionary. 49 | /// 50 | /// The validation result to store 51 | /// The ModelStateDictionary to store the errors in. 52 | /// An optional prefix. If omitted, the property names will be the keys. If specified, the prefix will be concatenated to the property name with a period. Eg "user.Name" 53 | public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState, string prefix) { 54 | if (!result.IsValid) { 55 | foreach (var error in result.Errors) { 56 | string key = string.IsNullOrEmpty(prefix) 57 | ? error.PropertyName 58 | : string.IsNullOrEmpty(error.PropertyName) 59 | ? prefix 60 | : prefix + "." + error.PropertyName; 61 | modelState.AddModelError(key, error.ErrorMessage); 62 | } 63 | } 64 | } 65 | 66 | /// 67 | /// Indicates which Rule Sets should be used when generating clientside messages. 68 | /// 69 | /// Http context 70 | /// Array of ruleset names 71 | public static void SetRulesetForClientsideMessages(this HttpContext context, params string[] ruleSets) => context.Items[_rulesetKey] = ruleSets; 72 | 73 | /// 74 | /// Gets the Rule Sets used to generate clientside validation metadata. 75 | /// 76 | /// Http context 77 | /// Array of ruleset names 78 | public static string[] GetRuleSetsForClientValidation(this HttpContext context) { 79 | // If the httpContext is null (for example, if IHttpContextProvider hasn't been registered) then just assume default ruleset. 80 | // This is OK because if we're actually using the attribute, the OnActionExecuting will have caught the fact that the provider is not registered. 81 | 82 | if (context?.Items != null && context.Items.ContainsKey(_rulesetKey) && context?.Items[_rulesetKey] is string[] ruleSets) { 83 | return ruleSets; 84 | } 85 | 86 | return new[] { RulesetValidatorSelector.DefaultRuleSetName }; 87 | } 88 | 89 | /// 90 | /// Indicates which Rule Sets should be used when generating clientside messages. 91 | /// 92 | /// Controller context 93 | /// Array of ruleset names 94 | public static void SetRulesetForClientsideMessages(this ControllerContext context, params string[] ruleSets) => context.HttpContext.SetRulesetForClientsideMessages(ruleSets); 95 | 96 | /// 97 | /// Indicates which Rule Sets should be used when generating clientside messages. 98 | /// 99 | /// Page context 100 | /// Array of ruleset names 101 | public static void SetRulesetForClientsideMessages(this PageContext context, params string[] ruleSets) => context.HttpContext.SetRulesetForClientsideMessages(ruleSets); 102 | } 103 | -------------------------------------------------------------------------------- /src/FluentValidation.AspNetCore/ValidatorDescriptorCache.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.AspNetCore; 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using Microsoft.AspNetCore.Http; 24 | using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; 25 | using Microsoft.Extensions.DependencyInjection; 26 | 27 | /// 28 | /// Caches the validators used when generating clientside metadata. 29 | /// Ideally, the validators would be singletons so this happens automatically, 30 | /// but we can't rely on this behaviour. The user may have registered them as something else 31 | /// And as of 10.0, the default is to auto-register validators as scoped as inexperienced developers 32 | /// often have issues understanding issues that arise from having singleton-scoped objects depending on non-singleton-scoped services 33 | /// Instead, we can cache the validators used for clientside validation in Httpcontext.Items to prevent them being instantiated once per property. 34 | /// 35 | internal class ValidatorDescriptorCache { 36 | private const string CacheKey = "_FV_ClientValidation_Cache"; 37 | 38 | public IValidatorDescriptor GetCachedDescriptor(ClientValidatorProviderContext context, IHttpContextAccessor httpContextAccessor) { 39 | if (httpContextAccessor == null) { 40 | throw new InvalidOperationException("Cannot use clientside validation unless the IHttpContextAccessor is registered with the service provider. Make sure the provider is registered by calling services.AddSingleton(); in your Startup class's ConfigureServices method"); 41 | } 42 | 43 | var modelType = context.ModelMetadata.ContainerType; 44 | if (modelType == null) return null; 45 | 46 | Dictionary cache = GetCache(httpContextAccessor.HttpContext.Items); 47 | 48 | if (cache.TryGetValue(modelType, out var descriptor)) { 49 | return descriptor; 50 | } 51 | 52 | IValidator validator; 53 | 54 | #pragma warning disable CS0618 55 | var validatorFactory = httpContextAccessor.HttpContext.RequestServices.GetService(); 56 | #pragma warning restore CS0618 57 | 58 | if (validatorFactory != null) { 59 | validator = validatorFactory.GetValidator(modelType); 60 | } 61 | else { 62 | validator = httpContextAccessor.HttpContext.RequestServices.GetService(modelType) as IValidator; 63 | } 64 | 65 | descriptor = validator?.CreateDescriptor(); 66 | cache[modelType] = descriptor; 67 | return descriptor; 68 | } 69 | 70 | private Dictionary GetCache(IDictionary httpContextItems) { 71 | Dictionary cache = null; 72 | 73 | if(httpContextItems.ContainsKey(CacheKey)) { 74 | cache = httpContextItems[CacheKey] as Dictionary; 75 | } 76 | 77 | if (cache == null) { 78 | cache = new Dictionary(); 79 | httpContextItems[CacheKey] = cache; 80 | } 81 | 82 | return cache; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Controllers/ApiTestController.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests.Controllers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class ApiTestController : Controller { 8 | 9 | [HttpPost] 10 | public ActionResult Create(TestModel test) { 11 | // Because this is an ApiController, the ModelStateInvalidFilter will prevent 12 | // this action from running and will return an automatically serialized ModelState response. 13 | return Ok(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Controllers/ClientsideController.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests.Controllers; 2 | 3 | using FluentValidation.AspNetCore; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | public class ClientsideController : Controller { 7 | public ActionResult Inputs() { 8 | return View(); 9 | } 10 | 11 | public ActionResult DefaultRuleset() { 12 | return View("RuleSet"); 13 | } 14 | 15 | [RuleSetForClientSideMessages("Foo")] 16 | public ActionResult SpecifiedRuleset() { 17 | return View("RuleSet"); 18 | } 19 | 20 | [RuleSetForClientSideMessages("Foo", "Bar")] 21 | public ActionResult MultipleRulesets() { 22 | return View("RuleSet"); 23 | } 24 | 25 | [RuleSetForClientSideMessages("Foo", "default")] 26 | public ActionResult DefaultAndSpecified() { 27 | return View("RuleSet"); 28 | } 29 | 30 | [RuleSetForClientSideMessages("*")] 31 | public ActionResult All() { 32 | return View("RuleSet"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests.Controllers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | public class HomeController : Controller { 6 | public ActionResult Index() => Content("Test"); 7 | } 8 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests.Controllers; 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using FluentValidation.AspNetCore; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.ModelBinding; 9 | 10 | public class TestController : Controller { 11 | public ActionResult SimpleFailure(SimpleModel model) { 12 | return TestResult(); 13 | } 14 | 15 | public ActionResult Test1(TestModel test) { 16 | return TestResult(); 17 | } 18 | 19 | public ActionResult Test1a(TestModel foo) { 20 | return TestResult(); 21 | } 22 | 23 | public ActionResult Test2(TestModel2 test) { 24 | return Content(test == null ? "null" : "not null"); 25 | } 26 | 27 | public ActionResult Test3(TestModel3 test) { 28 | return TestResult(); 29 | } 30 | 31 | public ActionResult Test4(TestModel4 test) { 32 | return TestResult(); 33 | } 34 | 35 | public ActionResult Test6(TestModel6 test) { 36 | return TestResult(); 37 | } 38 | 39 | public ActionResult WithoutValidator(TestModelWithoutValidator test) { 40 | return TestResult(); 41 | } 42 | 43 | public ActionResult TestModelWithOverridenMessageValueType(TestModelWithOverridenMessageValueType test) { 44 | return TestResult(); 45 | } 46 | 47 | public ActionResult TestModelWithOverridenPropertyNameValueType(TestModelWithOverridenPropertyNameValueType test) { 48 | return TestResult(); 49 | } 50 | 51 | public ActionResult RulesetTestModel(RulesetTestModel test) { 52 | return TestResult(); 53 | } 54 | 55 | public ActionResult Test5([FromBody] TestModel5 model) { 56 | return TestResult(); 57 | } 58 | 59 | public ActionResult Test5b(TestModel5 model) 60 | { 61 | return TestResult(); 62 | } 63 | 64 | public ActionResult RulesetTest([CustomizeValidator(RuleSet = "Names")] RulesetTestModel test) { 65 | return TestResult(); 66 | } 67 | 68 | public ActionResult PropertyTest([CustomizeValidator(Properties="Surname,Forename")]PropertiesTestModel test) { 69 | return TestResult(); 70 | } 71 | 72 | public ActionResult InterceptorTest([CustomizeValidator(Interceptor = typeof(SimplePropertyInterceptor))] PropertiesTestModel test) { 73 | return TestResult(); 74 | } 75 | 76 | public ActionResult ClearErrorsInterceptorTest([CustomizeValidator(Interceptor = typeof(ClearErrorsInterceptor))] PropertiesTestModel test) { 77 | return TestResult(); 78 | } 79 | 80 | public ActionResult BuiltInInterceptorTest(PropertiesTestModel2 test) { 81 | return TestResult(); 82 | } 83 | public ActionResult TwoParameters([CustomizeValidator(RuleSet = "Names")]RulesetTestModel first, RulesetTestModel second) { 84 | return TestResult(); 85 | } 86 | 87 | public ActionResult Lifecycle(LifecycleTestModel model) { 88 | return TestResult(); 89 | } 90 | 91 | public ActionResult Collection(List model) { 92 | return TestResult(); 93 | } 94 | 95 | public ActionResult MultipleErrors(MultipleErrorsModel model) { 96 | return TestResult(); 97 | } 98 | 99 | public ActionResult ModelThatimplementsIEnumerable(ModelThatImplementsIEnumerable model) { 100 | return TestResult(); 101 | } 102 | 103 | public ActionResult MultipleValidationStrategies(MultiValidationModel model) { 104 | return TestResult(); 105 | } 106 | 107 | public ActionResult MultipleValidationStrategies2(MultiValidationModel2 model) { 108 | return TestResult(); 109 | } 110 | 111 | public ActionResult MultipleValidationStrategies3(MultiValidationModel3 model) { 112 | return TestResult(); 113 | } 114 | 115 | public ActionResult DataAnnotations(DataAnnotationsModel model) { 116 | return TestResult(); 117 | } 118 | 119 | public ActionResult ImplicitChildValidator(ParentModel model) { 120 | return TestResult(); 121 | } 122 | 123 | public ActionResult ImplicitChildValidatorWithNullChild(ParentModel5 model) { 124 | return TestResult(); 125 | } 126 | 127 | public ActionResult ImplicitRootCollectionElementValidator([FromBody] IEnumerable model) { 128 | return TestResult(); 129 | } 130 | 131 | public ActionResult ImplicitRootCollectionElementValidationEnabled(ParentModel model) { 132 | return TestResult(); 133 | } 134 | 135 | public ActionResult ImplementsIValidatableObject(ImplementsIValidatableObjectModel model) { 136 | return TestResult(); 137 | } 138 | 139 | public ActionResult ImplicitChildImplementsIValidatableObject(ParentModel2 model) { 140 | return TestResult(); 141 | } 142 | 143 | public ActionResult ImplicitChildWithDataAnnotations(ParentModel3 model) { 144 | return TestResult(); 145 | } 146 | 147 | public ActionResult ImplicitAndExplicitChildValidator(ParentModel4 model) { 148 | return TestResult(); 149 | } 150 | 151 | public ActionResult ImplicitChildCollection([FromBody] ParentModel6 model) { 152 | return TestResult(); 153 | } 154 | 155 | public ActionResult ImplicitChildCollectionDataAnnotations([FromBody] ParentModel7 model) { 156 | return TestResult(); 157 | } 158 | 159 | #if !NETCOREAPP3_1 160 | public ActionResult ImplicitChildWithRecord([FromBody] ParentRecord model) { 161 | return TestResult(); 162 | } 163 | #endif 164 | 165 | public ActionResult CheckUnvalidated([FromBody] ParentModel6 model) { 166 | return Content(ModelState.Count(x => x.Value.ValidationState == ModelValidationState.Unvalidated).ToString()); 167 | } 168 | 169 | public ActionResult DictionaryParameter(Dictionary model) { 170 | return TestResult(); 171 | } 172 | 173 | public IActionResult UsingDictionaryWithJsonBody([FromBody]Dictionary model) 174 | { 175 | return TestResult(); 176 | } 177 | 178 | public IActionResult UsingEnumerable([FromBody]IEnumerable model) 179 | { 180 | return TestResult(); 181 | } 182 | 183 | public ActionResult SkipsValidation([CustomizeValidator(Skip=true)] TestModel test) { 184 | return TestResult(); 185 | } 186 | 187 | public ActionResult SkipsImplicitChildValidator([CustomizeValidator(Skip=true)] ParentModel model) { 188 | return TestResult(); 189 | } 190 | 191 | public ActionResult InjectsExplicitChildValidator(ParentModel model) { 192 | return TestResult(); 193 | } 194 | 195 | public ActionResult InjectsExplicitChildValidatorCollection(ParentModel6 model) { 196 | return TestResult(); 197 | } 198 | 199 | public ActionResult BadAsyncModel(BadAsyncModel model) { 200 | return TestResult(); 201 | } 202 | 203 | public async Task UpdateModel() { 204 | var model = new TestModel(); 205 | await TryUpdateModelAsync(model); 206 | return TestResult(); 207 | } 208 | 209 | public ActionResult AutoFilter(AutoFilterModel test) { 210 | return TestResult(); 211 | } 212 | 213 | public ActionResult AutoFilterParent(AutoFilterParentModel test) { 214 | return TestResult(); 215 | } 216 | 217 | public ActionResult AutoFilterRootCollection(List test) { 218 | return TestResult(); 219 | } 220 | 221 | public ActionResult AutoFilterParentWithCollection(AutoFilterParentWithCollectionModel test) { 222 | return TestResult(); 223 | } 224 | 225 | private ActionResult TestResult() { 226 | var errors = new List(); 227 | 228 | foreach (var pair in ModelState) { 229 | foreach (var error in pair.Value.Errors) { 230 | errors.Add(new SimpleError {Name = pair.Key, Message = error.ErrorMessage}); 231 | } 232 | } 233 | 234 | return Json(errors); 235 | } 236 | } 237 | 238 | 239 | public class SimpleError { 240 | public string Name { get; set; } 241 | public string Message { get; set; } 242 | 243 | public override string ToString() { 244 | return $"Property: {Name} Message: {Message}"; 245 | } 246 | } 247 | 248 | public class SimpleModel { 249 | public string Name { get; set; } 250 | public int Id { get; set; } 251 | } 252 | 253 | public static class TestHelper { 254 | public static bool IsValid(this List errors) { 255 | return errors.Count == 0; 256 | } 257 | 258 | public static bool IsValidField(this List errors, string name) { 259 | return errors.All(x => x.Name != name); 260 | } 261 | 262 | public static string GetError(this List errors, string name) { 263 | return errors.Where(x => x.Name == name).Select(x => x.Message).SingleOrDefault() ?? ""; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/CultureScope.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | namespace FluentValidation.Tests; 19 | 20 | using System; 21 | using System.Globalization; 22 | using System.Threading; 23 | 24 | public class CultureScope : IDisposable { 25 | CultureInfo _originalUiCulture; 26 | CultureInfo _originalCulture; 27 | 28 | public CultureScope(CultureInfo culture) { 29 | _originalCulture = Thread.CurrentThread.CurrentCulture; 30 | _originalUiCulture = Thread.CurrentThread.CurrentUICulture; 31 | 32 | Thread.CurrentThread.CurrentCulture = culture; 33 | Thread.CurrentThread.CurrentUICulture = culture; 34 | } 35 | 36 | public CultureScope(string culture) : this(new CultureInfo(culture)) { 37 | 38 | } 39 | 40 | public void Dispose() { 41 | Thread.CurrentThread.CurrentCulture = _originalCulture; 42 | Thread.CurrentThread.CurrentUICulture = _originalUiCulture; 43 | } 44 | 45 | public static void SetDefaultCulture() { 46 | Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); 47 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/DependencyInjectionTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Controllers; 7 | using FluentValidation.AspNetCore; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | 13 | #pragma warning disable CS0618 14 | 15 | #if !NET9_0 16 | public class DependencyInjectionTests : IClassFixture { 17 | private readonly ITestOutputHelper _output; 18 | private readonly HttpClient _client; 19 | 20 | public DependencyInjectionTests(ITestOutputHelper output, WebAppFixture webApp) { 21 | CultureScope.SetDefaultCulture(); 22 | 23 | _output = output; 24 | _client = webApp.WithWebHostBuilder(webHostBuilder => { 25 | webHostBuilder.ConfigureServices(services => { 26 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 27 | fv.ImplicitlyValidateChildProperties = false; 28 | }); 29 | services.AddSingleton(); 30 | services.AddScoped, InjectsExplicitChildValidator>(); 31 | services.AddScoped, InjectedChildValidator>(); 32 | services.AddScoped, InjectsExplicitChildValidatorCollection>(); 33 | }); 34 | }) 35 | .CreateClient(); 36 | } 37 | 38 | [Fact] 39 | public async Task Resolves_explicit_child_validator() { 40 | var result = await _client.GetErrors("InjectsExplicitChildValidator"); 41 | result.IsValidField("Child.Name").ShouldBeFalse(); 42 | result.GetError("Child.Name").ShouldEqual("NotNullInjected"); 43 | } 44 | 45 | [Fact] 46 | public async Task Resolves_explicit_child_validator_for_collection() { 47 | var formData = new Dictionary { 48 | {"Children[0].Name", null} 49 | }; 50 | var result = await _client.GetErrors("InjectsExplicitChildValidatorCollection", formData); 51 | result.IsValidField("Children[0].Name").ShouldBeFalse(); 52 | result.GetError("Children[0].Name").ShouldEqual("NotNullInjected"); 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/DisableAutoValidationTests.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.Tests; 20 | 21 | using System.Threading.Tasks; 22 | using Controllers; 23 | using FluentValidation.AspNetCore; 24 | using Microsoft.Extensions.DependencyInjection; 25 | using Xunit; 26 | using Xunit.Abstractions; 27 | 28 | public class DisableAutoValidationTests : IClassFixture { 29 | private WebAppFixture _webApp; 30 | 31 | public DisableAutoValidationTests(ITestOutputHelper output, WebAppFixture webApp) { 32 | _webApp = webApp; 33 | } 34 | 35 | [Fact] 36 | public async Task Disables_automatic_validation() { 37 | var client = _webApp.CreateClientWithServices(services => { 38 | #pragma warning disable CS0618 39 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 40 | fv.RegisterValidatorsFromAssemblyContaining(); 41 | fv.AutomaticValidationEnabled = false; 42 | }); 43 | #pragma warning restore CS0618 44 | }); 45 | 46 | var result = await client.GetErrors("InjectsExplicitChildValidator"); 47 | 48 | // Should be valid as automatic validation is completely disabled.. 49 | result.IsValidField("Child.Name").ShouldBeTrue(); 50 | } 51 | 52 | [Fact] 53 | public async Task Disables_automatic_validation_for_implicit_validation() { 54 | var client = _webApp.CreateClientWithServices(services => { 55 | #pragma warning disable CS0618 56 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 57 | fv.RegisterValidatorsFromAssemblyContaining(); 58 | fv.ImplicitlyValidateChildProperties = true; 59 | // Disabling auto validation supersedes enabling implicit validation. 60 | fv.AutomaticValidationEnabled = false; 61 | }); 62 | #pragma warning restore CS0618 63 | }); 64 | 65 | var result = await client.GetErrors("ImplicitChildValidator"); 66 | // Validation is disabled; no errors. 67 | result.Count.ShouldEqual(0); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/DisableDataAnnotationsTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using FluentValidation.AspNetCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Xunit; 7 | 8 | public class DisableDataAnnotationsTests : IClassFixture { 9 | private readonly WebAppFixture _app; 10 | 11 | public DisableDataAnnotationsTests(WebAppFixture app) { 12 | _app = app; 13 | } 14 | 15 | [Fact] 16 | public async Task Disables_data_annotations() { 17 | var client = _app.CreateClientWithServices(services => { 18 | #pragma warning disable CS0618 19 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 20 | fv.DisableDataAnnotationsValidation = true; 21 | }); 22 | #pragma warning restore CS0618 23 | services.AddScoped, MultiValidationValidator>(); 24 | }); 25 | 26 | var result = await client.GetErrors("MultipleValidationStrategies"); 27 | result.Count.ShouldEqual(1); 28 | result[0].Message.ShouldEqual("'Some Other Property' must not be empty."); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/ExtensionTests.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright (c) .NET Foundation and contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 18 | 19 | #endregion 20 | 21 | namespace FluentValidation.Tests; 22 | 23 | using FluentValidation.AspNetCore; 24 | using FluentValidation.Results; 25 | using Microsoft.AspNetCore.Mvc.ModelBinding; 26 | using Xunit; 27 | 28 | public class ValidationResultExtensionTests { 29 | private ValidationResult result; 30 | 31 | public ValidationResultExtensionTests() { 32 | result = new ValidationResult(new[] { 33 | new ValidationFailure("foo", "A foo error occurred", "x"), 34 | new ValidationFailure("bar", "A bar error occurred", "y"), 35 | new ValidationFailure("", "A nameless error occurred", "z"), 36 | }); 37 | } 38 | 39 | [Fact] 40 | public void Should_persist_to_modelstate() { 41 | var modelstate = new ModelStateDictionary(); 42 | result.AddToModelState(modelstate, null); 43 | 44 | modelstate.IsValid.ShouldBeFalse(); 45 | modelstate["foo"].Errors[0].ErrorMessage.ShouldEqual("A foo error occurred"); 46 | modelstate["bar"].Errors[0].ErrorMessage.ShouldEqual("A bar error occurred"); 47 | modelstate[""].Errors[0].ErrorMessage.ShouldEqual("A nameless error occurred"); 48 | } 49 | 50 | [Fact] 51 | public void Should_persist_modelstate_with_empty_prefix() { 52 | var modelstate = new ModelStateDictionary(); 53 | result.AddToModelState(modelstate, ""); 54 | modelstate["foo"].Errors[0].ErrorMessage.ShouldEqual("A foo error occurred"); 55 | modelstate[""].Errors[0].ErrorMessage.ShouldEqual("A nameless error occurred"); 56 | } 57 | 58 | [Fact] 59 | public void Should_persist_to_modelstate_with_prefix() { 60 | var modelstate = new ModelStateDictionary(); 61 | result.AddToModelState(modelstate, "baz"); 62 | 63 | modelstate.IsValid.ShouldBeFalse(); 64 | modelstate["baz.foo"].Errors[0].ErrorMessage.ShouldEqual("A foo error occurred"); 65 | modelstate["baz.bar"].Errors[0].ErrorMessage.ShouldEqual("A bar error occurred"); 66 | modelstate["baz"].Errors[0].ErrorMessage.ShouldEqual("A nameless error occurred"); 67 | } 68 | 69 | [Fact] 70 | public void Should_do_nothing_if_result_is_valid() { 71 | var modelState = new ModelStateDictionary(); 72 | new ValidationResult().AddToModelState(modelState, null); 73 | modelState.IsValid.ShouldBeTrue(); 74 | } 75 | 76 | [Fact] 77 | public void Does_not_overwrite_existing_values() { 78 | var modelstate = new ModelStateDictionary(); 79 | modelstate.AddModelError("model.Foo", "Foo"); 80 | 81 | result.AddToModelState(modelstate, "model"); 82 | 83 | modelstate["model.Foo"].Errors.Count.ShouldEqual(2); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/FluentValidation.Tests.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0;net7.0;net9.0 4 | false 5 | FluentValidation.Tests 6 | true 7 | true 8 | true 9 | True 10 | 10 11 | $(NoWarn);1701;1702;1591 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | TestMessages.resx 41 | True 42 | True 43 | 44 | 45 | 46 | 47 | TestMessages.Designer.cs 48 | PublicResXFileCodeGenerator 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/GlobalInterceptorTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Controllers; 6 | using FluentValidation.AspNetCore; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Newtonsoft.Json; 9 | using Xunit; 10 | 11 | public class GlobalInterceptorTests : IClassFixture { 12 | private WebAppFixture _app; 13 | 14 | public GlobalInterceptorTests(WebAppFixture app) { 15 | _app = app; 16 | } 17 | 18 | [Fact] 19 | public async Task When_global_action_context_interceptor_specified_Intercepts_validation_for_razor_pages() { 20 | var form = new Dictionary { 21 | {"Email", "foo"}, 22 | {"Surname", "foo"}, 23 | {"Forename", "foo"}, 24 | }; 25 | var client = _app.CreateClientWithServices(services => { 26 | #pragma warning disable CS0618 27 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(); 28 | #pragma warning restore CS0618 29 | services.AddScoped, RulesetTestValidator>(); 30 | services.AddSingleton(); 31 | }); 32 | var response = await client.PostResponse($"/RulesetTest", form); 33 | var result = JsonConvert.DeserializeObject>(response); 34 | 35 | result.IsValidField("Forename").ShouldBeFalse(); 36 | result.IsValidField("Surname").ShouldBeFalse(); 37 | result.IsValidField("Email").ShouldBeTrue(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Xml.Linq; 10 | using Controllers; 11 | using Newtonsoft.Json; 12 | 13 | public static class HttpClientExtensions { 14 | 15 | public static async Task GetResponse(this HttpClient client, string url, 16 | string querystring = "") { 17 | if (!String.IsNullOrEmpty(querystring)) { 18 | url += "?" + querystring; 19 | } 20 | 21 | var response = await client.GetAsync(url); 22 | response.EnsureSuccessStatusCode(); 23 | return await response.Content.ReadAsStringAsync(); 24 | } 25 | 26 | public static async Task PostResponse(this HttpClient client, string url, 27 | Dictionary form = null) { 28 | var c = new FormUrlEncodedContent(form ?? new()); 29 | 30 | var response = await client.PostAsync(url, c); 31 | response.EnsureSuccessStatusCode(); 32 | 33 | return await response.Content.ReadAsStringAsync(); 34 | } 35 | 36 | public static async Task> GetErrors(this HttpClient client, string action, Dictionary form = null) { 37 | var response = await client.PostResponse($"/Test/{action}", form); 38 | return JsonConvert.DeserializeObject>(response); 39 | } 40 | 41 | public static Task> GetErrorsViaJSON(this HttpClient client, string action, T model) { 42 | return client.GetErrorsViaJSONRaw(action, JsonConvert.SerializeObject(model)); 43 | } 44 | 45 | public static async Task> GetErrorsViaJSONRaw(this HttpClient client, string action, string json) { 46 | var request = new HttpRequestMessage(HttpMethod.Post, $"/Test/{action}"); 47 | request.Content = new StringContent(json, Encoding.UTF8, "application/json"); 48 | var responseMessage = await client.SendAsync(request); 49 | responseMessage.EnsureSuccessStatusCode(); 50 | var response = await responseMessage.Content.ReadAsStringAsync(); 51 | return JsonConvert.DeserializeObject>(response); 52 | } 53 | 54 | public static async Task GetClientsideMessages(this HttpClient client, string action = "/Clientside/Inputs") { 55 | var output = await client.GetResponse(action); 56 | return XDocument.Parse(output); 57 | } 58 | 59 | public static async Task GetClientsideMessage(this HttpClient client, string name, string attribute) { 60 | var doc = await client.GetClientsideMessages(); 61 | var elem = doc.Root.Elements("input") 62 | .Where(x => x.Attribute("name").Value == name).SingleOrDefault(); 63 | 64 | if (elem == null) { 65 | throw new Exception("Could not find element with name " + name); 66 | } 67 | 68 | var attr = elem.Attribute(attribute); 69 | 70 | if (attr == null || string.IsNullOrEmpty(attr.Value)) { 71 | throw new Exception("Could not find attr " + attribute); 72 | } 73 | 74 | return attr.Value; 75 | } 76 | 77 | public static async Task RunRulesetAction(this HttpClient client, string action, string modelPrefix = null) { 78 | 79 | var doc = await client.GetClientsideMessages(action); 80 | 81 | var elems = doc.Root.Elements("input") 82 | .Where(x => x.Attribute("name").Value.StartsWith($"{(modelPrefix == null ? string.Empty : $"{modelPrefix}.")}CustomName")); 83 | 84 | var results = elems.Select(x => x.Attribute("data-val-required")) 85 | .Where(x => x != null) 86 | .Select(x => x.Value) 87 | .ToArray(); 88 | 89 | return results; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/ImplicitRootCollectionTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Net.Http; 4 | using Controllers; 5 | using FluentValidation.AspNetCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | 9 | #pragma warning disable CS0618 10 | 11 | public class ImplicitRootCollectionTests : IClassFixture { 12 | private readonly WebAppFixture _app; 13 | 14 | public ImplicitRootCollectionTests(WebAppFixture app) { 15 | _app = app; 16 | } 17 | 18 | private HttpClient CreateClient(bool implicitCollectionValidationEnabled) { 19 | return _app.CreateClientWithServices(services => { 20 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 21 | fv.ImplicitlyValidateRootCollectionElements = implicitCollectionValidationEnabled; 22 | }); 23 | services.AddScoped, ParentModelValidator>(); 24 | services.AddScoped, ChildModelValidator>(); 25 | }); 26 | } 27 | 28 | [Fact] 29 | public async void Does_not_implicitly_run_root_collection_element_validator_when_disabled() { 30 | var client = CreateClient(false); 31 | var result = await client.GetErrorsViaJSON( 32 | nameof(TestController.ImplicitRootCollectionElementValidator), 33 | new[] { new ChildModel() }); 34 | 35 | result.Count.ShouldEqual(0); 36 | } 37 | 38 | [Fact] 39 | public async void Does_not_implicitly_run_child_validator_when_root_collection_element_validation_enabled() { 40 | var client = CreateClient(true); 41 | var result = await client.GetErrorsViaJSON( 42 | nameof(TestController.ImplicitRootCollectionElementValidationEnabled), 43 | new ParentModel()); 44 | 45 | result.Count.ShouldEqual(0); 46 | } 47 | 48 | [Fact] 49 | public async void Executes_implicit_root_collection_element_validator_when_enabled() { 50 | var client = CreateClient(true); 51 | var result = await client.GetErrorsViaJSON( 52 | nameof(TestController.ImplicitRootCollectionElementValidator), 53 | new[] { new ChildModel() }); 54 | 55 | result.Count.ShouldEqual(1); 56 | result[0].Name.ShouldEqual("[0].Name"); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/ImplicitValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Controllers; 7 | using FluentValidation.AspNetCore; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Newtonsoft.Json; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | using FormData = System.Collections.Generic.Dictionary; 13 | 14 | #pragma warning disable CS0618 15 | 16 | public class ImplicitValidationTests : IClassFixture { 17 | private WebAppFixture _app; 18 | private ITestOutputHelper _output; 19 | 20 | public ImplicitValidationTests(WebAppFixture app, ITestOutputHelper output) { 21 | _app = app; 22 | _output = output; 23 | } 24 | 25 | private HttpClient CreateClient(bool implicitValidationEnabled) { 26 | return _app.CreateClientWithServices(services => { 27 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 28 | fv.ImplicitlyValidateChildProperties = implicitValidationEnabled; 29 | }); 30 | services.AddScoped, ParentModelValidator>(); 31 | services.AddScoped, ParentModel2Validator>(); 32 | services.AddScoped, ParentModelValidator3>(); 33 | services.AddScoped, ParentModel4Validator>(); 34 | services.AddScoped, ParentModel5Validator>(); 35 | services.AddScoped, ChildModelValidator>(); 36 | services.AddScoped, ChildModel2Validator>(); 37 | services.AddScoped, ChildModelValidator3>(); 38 | services.AddScoped, ChildModel4Validator>(); 39 | services.AddScoped, CollectionTestModelValidator>(); 40 | services.AddScoped, TestModelValidator>(); 41 | services.AddScoped, TestModel5Validator>(); 42 | #if !NETCOREAPP3_1 43 | services.AddScoped, ParentRecordValidator>(); 44 | services.AddScoped, ChildRecordValidator>(); 45 | #endif 46 | }); 47 | } 48 | 49 | [Fact] 50 | public async void Does_not_implicitly_run_child_validator() { 51 | var client = CreateClient(false); 52 | var result = await client.GetErrors("ImplicitChildValidator", new FormData()); 53 | result.Count.ShouldEqual(0); 54 | } 55 | 56 | [Fact] 57 | public async void Implicitly_run_child_validator() { 58 | var client = CreateClient(true); 59 | var result = await client.GetErrors("ImplicitChildValidator", new FormData()); 60 | result.Count.ShouldEqual(1); 61 | result[0].Name.ShouldEqual("Child.Name"); 62 | } 63 | 64 | [Fact] 65 | public async void Ignores_null_child() { 66 | var client = CreateClient(true); 67 | var result = await client.GetErrors("ImplicitChildValidatorWithNullChild", new FormData()); 68 | result.Count.ShouldEqual(0); 69 | } 70 | 71 | [Fact] 72 | public async void Executes_implicit_child_validator_and_mixes_with_IValidatableObject() { 73 | var client = CreateClient(true); 74 | var result = await client.GetErrors("ImplicitChildImplementsIValidatableObject", new FormData()); 75 | result.Count.ShouldEqual(3); 76 | } 77 | 78 | [Fact] 79 | public async void Executes_implicit_child_validator_when_enabled_does_not_execute_multiple_times() { 80 | var client = CreateClient(true); 81 | var result = await client.GetErrors("ImplicitChildValidator", new FormData()); 82 | result.Count.ShouldEqual(1); 83 | 84 | result = await client.GetErrors("ImplicitChildValidator", new FormData()); 85 | result.Count.ShouldEqual(1); 86 | } 87 | 88 | [Fact] 89 | public async void ImplicitValidation_enabled_but_validator_explicitly_only_includes_error_message_once() { 90 | var client = CreateClient(true); 91 | var result = await client.GetErrors("ImplicitAndExplicitChildValidator", new FormData()); 92 | result.Count.ShouldEqual(1); 93 | } 94 | 95 | [Fact] 96 | public async void Executes_implicit_child_validator_and_mixes_with_DataAnnotations() { 97 | var client = CreateClient(true); 98 | var result = await client.GetErrors("ImplicitChildWithDataAnnotations", new FormData()); 99 | _output.WriteLine(JsonConvert.SerializeObject(result)); 100 | result.Count.ShouldEqual(2); 101 | } 102 | 103 | [Fact] 104 | public async void Can_validate_dictionary() { 105 | var client = CreateClient(true); 106 | var dictionary = new Dictionary() { 107 | {123, new TestModel5() {SomeBool = true, Id = 1}}, 108 | {456, new TestModel5()} 109 | }; 110 | var result = await client.GetErrorsViaJSON("UsingDictionaryWithJsonBody", dictionary); 111 | result.Count.ShouldEqual(2); 112 | result.IsValidField("[1].Value.Id").ShouldBeFalse(); 113 | result.IsValidField("[1].Value.SomeBool").ShouldBeFalse(); 114 | } 115 | 116 | [Fact] 117 | public async void Validates_dictionary_with_prefix() { 118 | var form = new FormData { 119 | {"model[0].Key", "0"}, 120 | {"model[0].Value.Name", null}, 121 | 122 | {"model[1].Key", "1"}, 123 | {"model[1].Value.Name", null}, 124 | 125 | {"model[2].Key", "2"}, 126 | {"model[2].Value.Name", "boop"} 127 | }; 128 | var client = CreateClient(true); 129 | var result = await client.GetErrors("DictionaryParameter", form); 130 | _output.WriteLine(JsonConvert.SerializeObject(result)); 131 | 132 | result.Count.ShouldEqual(2); 133 | } 134 | 135 | [Fact] 136 | public async void Validates_dictionary_without_prefix() { 137 | var form = new FormData { 138 | {"[0].Name", null}, 139 | {"[1].Name", null}, 140 | {"[2].Name", "whoop"}, 141 | }; 142 | var client = CreateClient(true); 143 | var result = await client.GetErrors("DictionaryParameter", form); 144 | _output.WriteLine(JsonConvert.SerializeObject(result)); 145 | 146 | result.Count.ShouldEqual(2); 147 | } 148 | 149 | [Fact] 150 | public async void Can_validate_enumerable() { 151 | var list = new List() { 152 | new TestModel5() {SomeBool = true, Id = 1}, 153 | new TestModel5(), 154 | new TestModel5() {SomeBool = true} 155 | }; 156 | 157 | var client = CreateClient(true); 158 | var result = await client.GetErrorsViaJSON("UsingEnumerable", list); 159 | 160 | result.IsValidField("[1].Id").ShouldBeFalse(); 161 | result.IsValidField("[1].SomeBool").ShouldBeFalse(); 162 | result.IsValidField("[2].Id").ShouldBeFalse(); 163 | result.Count.ShouldEqual(3); 164 | } 165 | 166 | [Fact] 167 | public async Task Validates_collection() { 168 | var form = new FormData { 169 | {"model[0].Name", "foo"}, 170 | {"model[1].Name", "foo"}, 171 | }; 172 | 173 | var client = CreateClient(true); 174 | var result = await client.GetErrors("Collection", form); 175 | 176 | result.Count.ShouldEqual(2); 177 | result[0].Name.ShouldEqual("model[0].Name"); 178 | } 179 | 180 | [Fact] 181 | public async Task Validates_collection_without_prefix() { 182 | var form = new FormData { 183 | {"[0].Name", "foo"}, 184 | {"[1].Name", "foo"}, 185 | }; 186 | 187 | var client = CreateClient(true); 188 | var result = await client.GetErrors("Collection", form); 189 | 190 | result.Count.ShouldEqual(2); 191 | result[0].Name.ShouldEqual("[0].Name"); 192 | } 193 | 194 | [Fact] 195 | public async void Skips_implicit_child_validation() { 196 | var result = await CreateClient(true).GetErrors("SkipsImplicitChildValidator", new FormData()); 197 | result.Count.ShouldEqual(0); 198 | } 199 | #if !NETCOREAPP3_1 200 | [Fact] 201 | public async void Does_not_run_child_validator_when_implicit_child_validation_disabled_for_record() { 202 | var json = @"{""Name"": ""Foo"", ""Child"": { ""Count"": 0 } }"; 203 | var client = CreateClient(false); 204 | var result = await client.GetErrorsViaJSONRaw("ImplicitChildWithRecord", json); 205 | result.Count.ShouldEqual(0); 206 | } 207 | 208 | [Fact] 209 | public async void Runs_child_validator_when_implicit_child_validation_enabled_for_record() { 210 | var json = @"{""Name"": ""Foo"", ""Child"": { ""Count"": 0 } }"; 211 | 212 | var client = CreateClient(true); 213 | var result = await client.GetErrorsViaJSONRaw("ImplicitChildWithRecord", json); 214 | result.Count.ShouldEqual(1); 215 | result[0].Name.ShouldEqual("Child.Count"); 216 | } 217 | #endif 218 | } 219 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/RulesetTest.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.RulesetTestPageModel 3 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/Rulesets/DefaultAndSpecifiedRuleSet.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithDefaultAndSpecifiedRuleSet 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/Rulesets/DefaultRuleSet.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithDefaultRuleSet 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/Rulesets/MultipleRuleSets.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithMultipleRuleSets 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/Rulesets/RuleSetForHandlers.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithRuleSetForHandlers 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/Rulesets/SpecifiedRuleSet.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithSpecifiedRuleSet 3 | 4 |
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/TestPage1.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModel 3 | 4 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/TestPageWithPrefix.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FluentValidation.Tests.TestPageModelWithPrefix -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using FluentValidation.Tests 2 | @namespace FluentValidation.Tests.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/RazorPagesTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using Controllers; 6 | using FluentValidation.AspNetCore; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Newtonsoft.Json; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | 13 | 14 | public class RazorPagesTestsWithImplicitValidationDisabled : IClassFixture { 15 | private readonly ITestOutputHelper _output; 16 | private readonly HttpClient _client; 17 | 18 | public RazorPagesTestsWithImplicitValidationDisabled(ITestOutputHelper output, WebAppFixture webApp) { 19 | CultureScope.SetDefaultCulture(); 20 | 21 | _output = output; 22 | _client = webApp.CreateClientWithServices(services => { 23 | #pragma warning disable CS0618 24 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(); 25 | #pragma warning restore CS0618 26 | services.AddScoped, TestModelValidator>(); 27 | services.AddScoped, RulesetTestValidator>(); 28 | services.AddScoped, ClientsideRulesetValidator>(); 29 | }); 30 | } 31 | 32 | [Fact] 33 | public async void Validates_with_BindProperty_attribute_when_implicit_validation_disabled() { 34 | var form = new Dictionary { 35 | {"Name", null}, 36 | }; 37 | 38 | var result = await _client.PostResponse("/TestPage1", form); 39 | var errors = JsonConvert.DeserializeObject>(result); 40 | 41 | errors.Count.ShouldEqual(1); 42 | } 43 | 44 | [Fact] 45 | public async void Validates_with_BindProperty_attribute_when_implicit_validation_disabled_using_prefix() { 46 | var form = new Dictionary { 47 | {"Test.Name", null}, 48 | }; 49 | 50 | var result = await _client.PostResponse("/TestPageWithPrefix", form); 51 | var errors = JsonConvert.DeserializeObject>(result); 52 | 53 | errors.Count.ShouldEqual(1); 54 | } 55 | 56 | [Fact] 57 | public async void Should_only_validate_specified_ruleset() { 58 | var form = new Dictionary { 59 | {"Email", "foo"}, 60 | {"Surname", "foo"}, 61 | {"Forename", "foo"}, 62 | }; 63 | 64 | var result = await _client.PostResponse("/RuleSetTest", form); 65 | var errors = JsonConvert.DeserializeObject>(result); 66 | 67 | errors.IsValidField("Forename").ShouldBeFalse(); 68 | errors.IsValidField("Surname").ShouldBeFalse(); 69 | errors.IsValidField("Email").ShouldBeTrue(); 70 | } 71 | } 72 | 73 | public class RazorPagesTestsWithImplicitValidationEnabled : IClassFixture { 74 | private readonly ITestOutputHelper _output; 75 | private readonly HttpClient _client; 76 | 77 | public RazorPagesTestsWithImplicitValidationEnabled(ITestOutputHelper output, WebAppFixture webApp) { 78 | CultureScope.SetDefaultCulture(); 79 | 80 | _output = output; 81 | _client = _client = webApp.CreateClientWithServices(services => { 82 | #pragma warning disable CS0618 83 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 84 | fv.ImplicitlyValidateChildProperties = true; 85 | }); 86 | #pragma warning restore CS0618 87 | services.AddScoped, TestModelValidator>(); 88 | services.AddScoped, RulesetTestValidator>(); 89 | services.AddScoped, ClientsideRulesetValidator>(); 90 | }); 91 | } 92 | 93 | [Fact] 94 | public async void Validates_with_BindProperty_attribute_when_implicit_validation_enabled() { 95 | var form = new Dictionary { 96 | {"Name", null}, 97 | }; 98 | 99 | var result = await _client.PostResponse("/TestPage1", form); 100 | var errors = JsonConvert.DeserializeObject>(result); 101 | 102 | errors.Count.ShouldEqual(1); 103 | } 104 | 105 | [Fact] 106 | public async void Validates_with_BindProperty_attribute_when_implicit_validation_disabled_using_prefix() { 107 | var form = new Dictionary { 108 | {"Test.Name", null}, 109 | }; 110 | 111 | var result = await _client.PostResponse("/TestPageWithPrefix", form); 112 | var errors = JsonConvert.DeserializeObject>(result); 113 | 114 | errors.Count.ShouldEqual(1); 115 | } 116 | 117 | [Fact] 118 | public async void Should_only_validate_specified_ruleset() { 119 | var form = new Dictionary { 120 | {"Email", "foo"}, 121 | {"Surname", "foo"}, 122 | {"Forename", "foo"}, 123 | }; 124 | 125 | var result = await _client.PostResponse("/RuleSetTest", form); 126 | var errors = JsonConvert.DeserializeObject>(result); 127 | 128 | errors.IsValidField("Forename").ShouldBeFalse(); 129 | errors.IsValidField("Surname").ShouldBeFalse(); 130 | errors.IsValidField("Email").ShouldBeTrue(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/ServiceCollectionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Linq; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Xunit; 6 | 7 | public class ServiceCollectionExtensionTests { 8 | public class TestClass { } 9 | public class TestValidator : AbstractValidator { } 10 | public class TestValidator2 : AbstractValidator { } 11 | internal class TestValidatorInternal : AbstractValidator { } 12 | 13 | [Fact] 14 | public void Should_resolve_validator_auto_registered_from_assembly_as_self() { 15 | var serviceProvider = new ServiceCollection() 16 | .AddValidatorsFromAssemblyContaining() 17 | .BuildServiceProvider(); 18 | 19 | serviceProvider.GetService().ShouldNotBeNull(); 20 | } 21 | 22 | [Fact] 23 | public void Should_resolve_validator_auto_registered_from_assembly_as_interface() { 24 | var serviceProvider = new ServiceCollection() 25 | .AddValidatorsFromAssemblyContaining() 26 | .BuildServiceProvider(); 27 | 28 | serviceProvider.GetService>().ShouldNotBeNull(); 29 | } 30 | 31 | [Fact] 32 | public void AddValidatorsFromAssemblyContaining_T_When_Instructed_Should_Add_Internal_Validators() { 33 | var serviceCollection = new ServiceCollection() 34 | .AddValidatorsFromAssemblyContaining(includeInternalTypes: true); 35 | 36 | Assert.Contains(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 37 | } 38 | 39 | [Fact] 40 | public void AddValidatorsFromAssemblyContaining_T_By_Default_Should_Not_Add_Internal_Validators() { 41 | var serviceCollection = new ServiceCollection() 42 | .AddValidatorsFromAssemblyContaining(); 43 | 44 | Assert.DoesNotContain(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 45 | } 46 | 47 | [Fact] 48 | public void AddValidatorsFromAssemblyContaining_When_Instructed_Should_Add_Internal_Validators() { 49 | var serviceCollection = new ServiceCollection() 50 | .AddValidatorsFromAssemblyContaining(typeof(ServiceCollectionExtensionTests), includeInternalTypes: true); 51 | 52 | Assert.Contains(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 53 | } 54 | 55 | [Fact] 56 | public void AddValidatorsFromAssemblyContaining_By_Default_Should_Not_Add_Internal_Validators() { 57 | var serviceCollection = new ServiceCollection() 58 | .AddValidatorsFromAssemblyContaining(typeof(ServiceCollectionExtensionTests)); 59 | 60 | Assert.DoesNotContain(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 61 | } 62 | 63 | [Fact] 64 | public void AddValidatorsFromAssembly_When_Instructed_Should_Add_Internal_Validators() { 65 | var serviceCollection = new ServiceCollection() 66 | .AddValidatorsFromAssembly(typeof(ServiceCollectionExtensionTests).Assembly, includeInternalTypes: true); 67 | 68 | Assert.Contains(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 69 | } 70 | 71 | [Fact] 72 | public void AddValidatorsFromAssembly_By_Default_Should_Not_Add_Internal_Validators() { 73 | var serviceCollection = new ServiceCollection() 74 | .AddValidatorsFromAssembly(typeof(ServiceCollectionExtensionTests).Assembly); 75 | 76 | Assert.DoesNotContain(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 77 | } 78 | 79 | [Fact] 80 | public void AddValidatorsFromAssemblies_When_Instructed_Should_Add_Internal_Validators() { 81 | var serviceCollection = new ServiceCollection() 82 | .AddValidatorsFromAssemblies(new[] { typeof(ServiceCollectionExtensionTests).Assembly }, includeInternalTypes: true); 83 | 84 | Assert.Contains(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 85 | } 86 | 87 | [Fact] 88 | public void AddValidatorsFromAssemblies_By_Default_Should_Not_Add_Internal_Validators() { 89 | var serviceCollection = new ServiceCollection() 90 | .AddValidatorsFromAssemblies(new[] { typeof(ServiceCollectionExtensionTests).Assembly }); 91 | 92 | Assert.DoesNotContain(serviceCollection, o => o.ImplementationType == typeof(TestValidatorInternal)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/ServiceProviderTests.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Controllers; 7 | using FluentValidation.AspNetCore; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Xunit; 10 | 11 | public class ServiceProviderTests : IClassFixture { 12 | private readonly HttpClient _client; 13 | 14 | public ServiceProviderTests(WebAppFixture webApp) { 15 | 16 | _client = webApp.CreateClientWithServices(services => { 17 | #pragma warning disable CS0618 18 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(); 19 | #pragma warning restore CS0618 20 | services.AddValidatorsFromAssemblyContaining(); 21 | }); 22 | } 23 | 24 | [Fact] 25 | public async Task Gets_validators_from_service_provider() { 26 | var form = new Dictionary { 27 | { "test.Name", null } 28 | }; 29 | 30 | var result = await _client.GetErrors("Test1", form); 31 | 32 | result.IsValidField("test.Name").ShouldBeFalse(); 33 | result.GetError("test.Name").ShouldEqual("Validation Failed"); 34 | } 35 | 36 | [Fact] 37 | public async Task Validators_should_be_scoped() { 38 | var result = await _client.GetErrors("Lifecycle"); 39 | var hashCode1 = result.GetError("Foo"); 40 | 41 | var result2 = await _client.GetErrors("Lifecycle"); 42 | var hashCode2 = result2.GetError("Foo"); 43 | 44 | Assert.NotNull(hashCode1); 45 | Assert.NotNull(hashCode2); 46 | Assert.NotEqual("", hashCode1); 47 | Assert.NotEqual("", hashCode2); 48 | 49 | Assert.NotEqual(hashCode1, hashCode2); 50 | } 51 | 52 | [Fact] 53 | public async Task Gets_validator_for_model_not_underlying_collection_type() { 54 | var result = await _client.GetErrors("ModelThatimplementsIEnumerable"); 55 | result.GetError("Name").ShouldEqual("Foo"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests.AspNetCore { 2 | using System.Globalization; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Localization; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | public class Startup { 8 | public void ConfigureServices(IServiceCollection services) { 9 | // Intentionally not implemented - each test fixture should configure services explicitly. 10 | } 11 | 12 | public void Configure(IApplicationBuilder app) { 13 | CultureInfo cultureInfo = new CultureInfo("en-US"); 14 | app.UseRequestLocalization(options => { 15 | options.DefaultRequestCulture = new RequestCulture(cultureInfo); 16 | options.SupportedCultures = new[] {cultureInfo}; 17 | options.SupportedUICultures = new[] {cultureInfo}; 18 | }); 19 | 20 | app 21 | .UseRouting() 22 | .UseEndpoints(endpoints => { 23 | endpoints.MapRazorPages(); 24 | endpoints.MapDefaultControllerRoute(); 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TestExtensions.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.Tests; 20 | 21 | using Xunit; 22 | 23 | //Inspired by SpecUnit's SpecificationExtensions 24 | //http://code.google.com/p/specunit-net/source/browse/trunk/src/SpecUnit/SpecificationExtensions.cs 25 | public static class TestExtensions { 26 | public static void ShouldEqual(this object actual, object expected) { 27 | Assert.Equal(expected, actual); 28 | } 29 | 30 | public static void ShouldBeTheSameAs(this object actual, object expected) { 31 | Assert.Same(expected, actual); 32 | } 33 | 34 | public static void ShouldBeNull(this object actual) { 35 | Assert.Null(actual); 36 | } 37 | 38 | public static void ShouldNotBeNull(this object actual) { 39 | Assert.NotNull(actual); 40 | } 41 | 42 | public static void ShouldBeTrue(this bool b) { 43 | Assert.True(b); 44 | } 45 | 46 | public static void ShouldBeTrue(this bool b, string msg) { 47 | Assert.True(b, msg); 48 | } 49 | 50 | public static void ShouldBeFalse(this bool b) { 51 | Assert.False(b); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TestMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FluentValidation.Tests { 12 | using System; 13 | using System.Reflection; 14 | 15 | 16 | /// 17 | /// A strongly-typed resource class, for looking up localized strings, etc. 18 | /// 19 | // This class was auto-generated by the StronglyTypedResourceBuilder 20 | // class via a tool like ResGen or Visual Studio. 21 | // To add or remove a member, edit your .ResX file then rerun ResGen 22 | // with the /str option, or rebuild your VS project. 23 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 24 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 25 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 26 | public class TestMessages { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal TestMessages() { 34 | } 35 | 36 | /// 37 | /// Returns the cached ResourceManager instance used by this class. 38 | /// 39 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 40 | public static global::System.Resources.ResourceManager ResourceManager { 41 | get { 42 | if (object.ReferenceEquals(resourceMan, null)) { 43 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FluentValidation.Tests.TestMessages", typeof(TestMessages).Assembly); 44 | resourceMan = temp; 45 | } 46 | return resourceMan; 47 | } 48 | } 49 | 50 | /// 51 | /// Overrides the current thread's CurrentUICulture property for all 52 | /// resource lookups using this strongly typed resource class. 53 | /// 54 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 55 | public static global::System.Globalization.CultureInfo Culture { 56 | get { 57 | return resourceCulture; 58 | } 59 | set { 60 | resourceCulture = value; 61 | } 62 | } 63 | 64 | /// 65 | /// Looks up a localized string similar to Localised Error. 66 | /// 67 | public static string notnull_error { 68 | get { 69 | return ResourceManager.GetString("notnull_error", resourceCulture); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized string similar to Test {0}. 75 | /// 76 | public static string PlaceholderMessage { 77 | get { 78 | return ResourceManager.GetString("PlaceholderMessage", resourceCulture); 79 | } 80 | } 81 | 82 | /// 83 | /// Looks up a localized string similar to foo. 84 | /// 85 | public static string PropertyName { 86 | get { 87 | return ResourceManager.GetString("PropertyName", resourceCulture); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TestMessages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Localised Error 122 | 123 | 124 | Test {0} 125 | 126 | 127 | foo 128 | 129 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TestModels_ClientSIde.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System; 4 | using Controllers; 5 | using Microsoft.Extensions.Localization; 6 | 7 | public class ClientsideModel { 8 | public string CreditCard { get; set; } 9 | public string Email { get; set; } 10 | public string EqualTo { get; set; } 11 | public string MaxLength { get; set; } 12 | public string MinLength { get; set; } 13 | public int Range { get; set; } 14 | public string RegEx { get; set; } 15 | public string Required { get; set; } 16 | public string Length { get; set; } 17 | public string Required2 { get; set; } 18 | public string RequiredInsidePartial { get; set; } 19 | public string ExactLength { get; set; } 20 | public int GreaterThan { get; set; } 21 | public int GreaterThanOrEqual { get; set; } 22 | public DateTime GreaterThanOrEqualProperty { get; set; } 23 | public DateTime GreaterThanOrEqualFunc { get; set; } 24 | public int LessThan { get; set; } 25 | public int LessThanOrEqual { get; set; } 26 | public DateTime LessThanOrEqualProperty { get; set; } 27 | public DateTime LessThanOrEqualFunc { get; set; } 28 | public DateTime DateTimeComparison { get; set; } 29 | public string LengthWithMessage { get; set; } 30 | public string CustomPlaceholder { get; set; } 31 | public string LengthCustomPlaceholders { get; set; } 32 | public string CustomName { get; set; } 33 | public string MessageWithContext { get; set; } 34 | public int CustomNameValueType { get; set; } 35 | public string LocalizedName { get; set; } 36 | public string LocalizedMessage { get; set; } 37 | } 38 | 39 | public class ClientsideRulesetModel { 40 | public string CustomName1 { get; set; } 41 | public string CustomName2 { get; set; } 42 | public string CustomName3 { get; set; } 43 | } 44 | 45 | public class ClientsideRulesetValidator : AbstractValidator { 46 | public ClientsideRulesetValidator() { 47 | RuleSet("Foo", () => { 48 | RuleFor(x => x.CustomName1).NotNull().WithMessage("first"); 49 | }); 50 | RuleSet("Bar", () => { 51 | RuleFor(x => x.CustomName2).NotNull().WithMessage("second"); 52 | }); 53 | 54 | RuleFor(x => x.CustomName3).NotNull().WithMessage("third"); 55 | 56 | } 57 | } 58 | 59 | public class ClientsideScopedDependency { } 60 | 61 | 62 | public class ClientsideModelValidator : AbstractValidator { 63 | public static int TimesInstantiated = 0; 64 | 65 | // Need to inject a scoped dependency here to validate that we allow scoped dependencies when generating clientside rules, as MvcViewOptionSetup is always resolved from root container. 66 | // So we may end up with a cannot resolve from root provider error if things aren't configured properly. 67 | public ClientsideModelValidator(ClientsideScopedDependency dep, IStringLocalizer localizer) { 68 | RuleFor(x => x.CreditCard).CreditCard(); 69 | RuleFor(x => x.Email).EmailAddress(); 70 | RuleFor(x => x.EqualTo).Equal(x => x.Required); 71 | RuleFor(x => x.MaxLength).MaximumLength(2); 72 | RuleFor(x => x.MinLength).MinimumLength(1); 73 | RuleFor(x => x.Range).InclusiveBetween(1, 5); 74 | RuleFor(x => x.RegEx).Matches("[0-9]"); 75 | RuleFor(x => x.Required).NotEmpty(); 76 | RuleFor(x => x.Required2).NotNull(); 77 | RuleFor(x => x.RequiredInsidePartial).NotEmpty(); 78 | 79 | RuleFor(x => x.Length).Length(1, 4); 80 | RuleFor(x => x.ExactLength).Length(4); 81 | RuleFor(x => x.LessThan).LessThan(10); 82 | RuleFor(x => x.LessThanOrEqual).LessThanOrEqualTo(10); 83 | RuleFor(x => x.GreaterThan).GreaterThan(1); 84 | RuleFor(x => x.GreaterThanOrEqual).GreaterThanOrEqualTo(1); 85 | RuleFor(x => x.LessThanOrEqualProperty).LessThanOrEqualTo(x => x.DateTimeComparison); 86 | RuleFor(x => x.GreaterThanOrEqualProperty).GreaterThanOrEqualTo(x => x.DateTimeComparison); 87 | RuleFor(x => x.LessThanOrEqualFunc).LessThanOrEqualTo(x => DateTime.Now); 88 | RuleFor(x => x.GreaterThanOrEqualFunc).GreaterThanOrEqualTo(x => DateTime.Now); 89 | 90 | RuleFor(x => x.LengthWithMessage).Length(1, 10).WithMessage("Foo"); 91 | RuleFor(x => x.CustomPlaceholder).NotNull().WithMessage("{PropertyName} is null."); 92 | RuleFor(x => x.LengthCustomPlaceholders).Length(1, 5).WithMessage("Must be between {MinLength} and {MaxLength}."); 93 | 94 | RuleFor(x => x.CustomName).NotNull().WithName("Foo"); 95 | RuleFor(x => x.LocalizedName).NotNull().WithName(x => TestMessages.notnull_error); 96 | RuleFor(x => x.CustomNameValueType).NotNull().WithName("Foo"); 97 | RuleFor(x => x.MessageWithContext).NotNull().WithMessage(x => $"Foo {x.Required}"); 98 | RuleFor(x => x.LocalizedMessage).NotNull().WithMessage(x => localizer["from localizer"]); 99 | 100 | 101 | TimesInstantiated++; 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TestPageModel.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests { 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AspNetCore; 5 | using Controllers; 6 | using FluentValidation.AspNetCore; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | [IgnoreAntiforgeryToken(Order = 1001)] 11 | public class TestPageModel : PageModel { 12 | 13 | [BindProperty] 14 | public TestModel Test { get; set; } 15 | 16 | public Task OnPostAsync() { 17 | return Task.FromResult(TestResult()); 18 | } 19 | 20 | private IActionResult TestResult() { 21 | var errors = new List(); 22 | 23 | foreach (var pair in ModelState) { 24 | foreach (var error in pair.Value.Errors) { 25 | errors.Add(new SimpleError { Name = pair.Key, Message = error.ErrorMessage }); 26 | } 27 | } 28 | 29 | return new JsonResult(errors); 30 | } 31 | } 32 | 33 | [IgnoreAntiforgeryToken(Order = 1001)] 34 | public class RulesetTestPageModel : PageModel { 35 | 36 | [BindProperty] 37 | [CustomizeValidator(RuleSet = "Names")] 38 | public RulesetTestModel Test { get; set; } 39 | 40 | public Task OnPostAsync() { 41 | return Task.FromResult(TestResult()); 42 | } 43 | 44 | private IActionResult TestResult() { 45 | var errors = new List(); 46 | 47 | foreach (var pair in ModelState) { 48 | foreach (var error in pair.Value.Errors) { 49 | errors.Add(new SimpleError { Name = pair.Key, Message = error.ErrorMessage }); 50 | } 51 | } 52 | 53 | return new JsonResult(errors); 54 | } 55 | } 56 | 57 | [IgnoreAntiforgeryToken(Order = 1001)] 58 | public class TestPageModelWithPrefix : PageModel { 59 | 60 | [BindProperty(Name = "Test")] 61 | public TestModel Test { get; set; } 62 | 63 | public Task OnPostAsync() { 64 | return Task.FromResult(TestResult()); 65 | } 66 | 67 | private IActionResult TestResult() { 68 | var errors = new List(); 69 | 70 | foreach (var pair in ModelState) { 71 | foreach (var error in pair.Value.Errors) { 72 | errors.Add(new SimpleError { Name = pair.Key, Message = error.ErrorMessage }); 73 | } 74 | } 75 | 76 | return new JsonResult(errors); 77 | } 78 | } 79 | 80 | [IgnoreAntiforgeryToken(Order = 1001)] 81 | public class TestPageModelWithDefaultRuleSet : PageModel { 82 | 83 | [BindProperty(Name = "Test")] 84 | public ClientsideRulesetModel Test { get; set; } 85 | 86 | public IActionResult OnGet() => Page(); 87 | } 88 | 89 | [IgnoreAntiforgeryToken(Order = 1001)] 90 | [RuleSetForClientSideMessages("Foo")] 91 | public class TestPageModelWithSpecifiedRuleSet : PageModel { 92 | 93 | [BindProperty(Name = "Test")] 94 | public ClientsideRulesetModel Test { get; set; } 95 | 96 | public IActionResult OnGet() => Page(); 97 | } 98 | 99 | [IgnoreAntiforgeryToken(Order = 1001)] 100 | [RuleSetForClientSideMessages("Foo", "Bar")] 101 | public class TestPageModelWithMultipleRuleSets : PageModel { 102 | 103 | [BindProperty(Name = "Test")] 104 | public ClientsideRulesetModel Test { get; set; } 105 | 106 | public IActionResult OnGet() => Page(); 107 | } 108 | 109 | [IgnoreAntiforgeryToken(Order = 1001)] 110 | [RuleSetForClientSideMessages("Foo", "default")] 111 | public class TestPageModelWithDefaultAndSpecifiedRuleSet : PageModel { 112 | 113 | [BindProperty(Name = "Test")] 114 | public ClientsideRulesetModel Test { get; set; } 115 | 116 | public IActionResult OnGet() => Page(); 117 | } 118 | 119 | [IgnoreAntiforgeryToken(Order = 1001)] 120 | public class TestPageModelWithRuleSetForHandlers : PageModel { 121 | 122 | [BindProperty(Name = "Test")] 123 | public ClientsideRulesetModel Test { get; set; } 124 | 125 | public IActionResult OnGetDefault() => Page(); 126 | 127 | public IActionResult OnGetSpecified() { 128 | PageContext.SetRulesetForClientsideMessages("Foo"); 129 | return Page(); 130 | } 131 | 132 | public IActionResult OnGetMultiple() { 133 | PageContext.SetRulesetForClientsideMessages("Foo", "Bar"); 134 | return Page(); 135 | } 136 | 137 | public IActionResult OnGetDefaultAndSpecified() { 138 | PageContext.SetRulesetForClientsideMessages("Foo", "default"); 139 | return Page(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/TypeFilterTests.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | // Copyright (c) .NET Foundation and contributors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // The latest version of this file can be found at https://github.com/FluentValidation/FluentValidation 17 | #endregion 18 | 19 | namespace FluentValidation.Tests; 20 | 21 | using System.Threading.Tasks; 22 | using AspNetCore; 23 | using Controllers; 24 | using FluentValidation.AspNetCore; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Xunit; 27 | using Xunit.Abstractions; 28 | 29 | #pragma warning disable CS0618 30 | 31 | public class TypeFilterTests : IClassFixture { 32 | private WebAppFixture _webApp; 33 | 34 | public TypeFilterTests(ITestOutputHelper output, WebAppFixture webApp) { 35 | _webApp = webApp; 36 | } 37 | 38 | [Fact] 39 | public async Task Finds_and_executes_validator() { 40 | var client = _webApp.CreateClientWithServices(services => { 41 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 42 | fv.RegisterValidatorsFromAssemblyContaining(); 43 | }); 44 | }); 45 | var result = await client.GetErrors("Test1"); 46 | 47 | // Validator was found and executed so field shouldn't be valid. 48 | result.IsValidField("Name").ShouldBeFalse(); 49 | 50 | } 51 | 52 | [Fact] 53 | public async Task Filters_types() { 54 | var client = _webApp.CreateClientWithServices(services => { 55 | services.AddMvc().AddNewtonsoftJson().AddFluentValidation(fv => { 56 | fv.RegisterValidatorsFromAssemblyContaining(scanResult => { 57 | return scanResult.ValidatorType != typeof(TestModelValidator); 58 | }); 59 | }); 60 | }); 61 | 62 | var result = await client.GetErrors("Test1"); 63 | 64 | // Should be valid as the validator was skipped. 65 | result.IsValidField("Name").ShouldBeTrue(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Views/Clientside/Inputs.cshtml: -------------------------------------------------------------------------------- 1 | @model FluentValidation.Tests.ClientsideModel 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | @Html.Partial("_TestPartial", Model) 31 |
32 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Views/Clientside/RuleSet.cshtml: -------------------------------------------------------------------------------- 1 | @model FluentValidation.Tests.ClientsideRulesetModel 2 |
3 | 4 | 5 | 6 |
-------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Views/Clientside/_TestPartial.cshtml: -------------------------------------------------------------------------------- 1 | @model FluentValidation.Tests.ClientsideModel 2 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/WebAppFixture.cs: -------------------------------------------------------------------------------- 1 | namespace FluentValidation.Tests; 2 | 3 | using System; 4 | using System.Net.Http; 5 | using AspNetCore; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc.Testing; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | public class WebAppFixture : WebApplicationFactory { 11 | protected override void ConfigureWebHost(IWebHostBuilder builder) { 12 | builder.UseContentRoot("."); 13 | } 14 | 15 | protected override IWebHostBuilder CreateWebHostBuilder() { 16 | return new WebHostBuilder() 17 | .UseDefaultServiceProvider((context, options) => options.ValidateScopes = true) 18 | .UseStartup(); 19 | } 20 | 21 | public HttpClient CreateClientWithServices(Action configurator) { 22 | return WithWebHostBuilder(builder => builder.ConfigureServices(configurator)).CreateClient(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/FluentValidation.Tests.AspNetCore/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxParallelThreads": 1, 3 | "parallelizeAssembly": false, 4 | "parallelizeTestCollections": false, 5 | "preEnumerateTheories": false, 6 | "shadowCopy": false 7 | } --------------------------------------------------------------------------------