From 505da81b9750a2698b7526ca316f74c8668f0e84 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:44:23 -0400 Subject: [PATCH 01/13] remove obsolete validation attribute (#68) --- .../FluentValidationAutoValidationAttribute.cs | 10 ---------- .../FluentValidationAutoValidationActionFilter.cs | 5 +---- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs deleted file mode 100644 index 666edde..0000000 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes -{ - [Obsolete("Attribute is obsolete and will be removed in v2. Use the [AutoValidation] attribute instead.")] - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class FluentValidationAutoValidationAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index c6a7d79..a3e2c16 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -38,10 +38,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; var serviceProvider = actionExecutingContext.HttpContext.RequestServices; - if (endpoint != null && - ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && - !endpoint.Metadata.OfType().Any() && !endpoint.Metadata.OfType().Any()) || - endpoint.Metadata.OfType().Any())) + if (endpoint != null && ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && !endpoint.Metadata.OfType().Any()) || endpoint.Metadata.OfType().Any())) { HandleUnvalidatedEntries(actionExecutingContext); From 9aab2e28abc88ad3e4a3ea9f673d3e99f71cc160 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:44:06 -0400 Subject: [PATCH 02/13] change supported target frameworks (#70) --- .github/workflows/Build.yaml | 10 +++--- .github/workflows/Release.yaml | 6 ++-- Directory.Build.props | 1 + ...Validation.AutoValidation.Endpoints.csproj | 1 - ...FluentValidation.AutoValidation.Mvc.csproj | 1 - .../AutoValidationMvcConfiguration.cs | 4 +-- .../src/Enums/ValidationStrategy.cs | 2 +- ...entValidation.AutoValidation.Shared.csproj | 1 - ...uentValidation.AutoValidation.Tests.csproj | 13 ++++---- .../ServiceCollectionExtensionsTest.cs | 4 ++- ...idationAutoValidationEndpointFilterTest.cs | 4 ++- ...nAutoValidationDefaultResultFactoryTest.cs | 4 ++- .../ServiceCollectionExtensionsTest.cs | 4 ++- ...alidationAutoValidationActionFilterTest.cs | 31 ++++++------------- ...nAutoValidationDefaultResultFactoryTest.cs | 10 +++--- .../ServiceProviderExtensionsTest.cs | 4 ++- .../Extensions/TypeExtensionsTest.cs | 4 ++- .../ValidationResultExtensionsTest.cs | 4 ++- 18 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 7594aaf..d4d5708 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -4,11 +4,9 @@ env: JAVA_VERSION: 17 JAVA_DISTRIBUTION: microsoft DOTNET_VERSION: | - 3.1.x - 6.0.x - 7.0.x 8.0.x 9.0.x + 10.0.x DOTNET_BUILD_CONFIGURATION: Release SONAR_PATH: .\.sonar\scanner SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -29,18 +27,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Set up .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index a970688..a46c4f5 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -1,7 +1,7 @@ name: FluentValidation.AutoValidation [Release] env: - DOTNET_VERSION: 9.0.x + DOTNET_VERSION: 10.0.x DOTNET_BUILD_CONFIGURATION: Release DOTNET_PACKAGES_OUTPUT_DIRECTORY: .nuget NUGET_SOURCE: https://api.nuget.org/v3/index.json @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/Directory.Build.props b/Directory.Build.props index ff8f997..b7ebf93 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ + net8.0;net9.0;net10.0 enable 8.0 NU1701 diff --git a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj index 5019ec4..44b56d1 100644 --- a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj +++ b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj @@ -5,7 +5,6 @@ - net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Endpoints SharpGrip.FluentValidation.AutoValidation.Endpoints SharpGrip FluentValidation AutoValidation Endpoints diff --git a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj index 4117c6f..2b39e94 100644 --- a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj +++ b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj @@ -5,7 +5,6 @@ - netcoreapp3.1;net6.0;net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Mvc SharpGrip.FluentValidation.AutoValidation.Mvc SharpGrip FluentValidation AutoValidation MVC diff --git a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs index 0d86cc3..ae56f21 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs @@ -16,7 +16,7 @@ public class AutoValidationMvcConfiguration /// /// Configures the validation strategy. Validation strategy enables asynchronous automatic validation on all controllers inheriting from . - /// Validation strategy enables asynchronous automatic validation on controllers inheriting from decorated (class or method) with a attribute. + /// Validation strategy enables asynchronous automatic validation on controllers decorated (class or method) with a attribute. /// public ValidationStrategy ValidationStrategy { get; set; } = ValidationStrategy.All; @@ -70,7 +70,7 @@ public class AutoValidationMvcConfiguration /// The default result factory returns the default object wrapped in a >. /// /// - /// The custom result factory implement . + /// The custom result factory implementing . public void OverrideDefaultResultFactoryWith() where TResultFactory : IFluentValidationAutoValidationResultFactory { OverriddenResultFactory = typeof(TResultFactory); diff --git a/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs b/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs index afdd392..047de58 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs @@ -11,7 +11,7 @@ public enum ValidationStrategy All = 1, /// - /// Enables asynchronous automatic validation on controllers inheriting from decorated with a attribute. + /// Enables asynchronous automatic validation on controllers inheriting from decorated with a attribute. /// Annotations = 2 } diff --git a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj index eed5b58..d7cf986 100644 --- a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj +++ b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj @@ -5,7 +5,6 @@ - netstandard2.0;netstandard2.1;netcoreapp3.1;net6.0;net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Shared SharpGrip.FluentValidation.AutoValidation.Shared SharpGrip FluentValidation AutoValidation Shared diff --git a/Tests/FluentValidation.AutoValidation.Tests.csproj b/Tests/FluentValidation.AutoValidation.Tests.csproj index 0f5fe11..e549e39 100644 --- a/Tests/FluentValidation.AutoValidation.Tests.csproj +++ b/Tests/FluentValidation.AutoValidation.Tests.csproj @@ -1,33 +1,32 @@ - net7.0;net8.0;net9.0 latest false SharpGrip.FluentValidation.AutoValidation.Tests - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs index 78bf5d1..d5edb2c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,4 +1,6 @@ -using FluentValidation.Results; +// ReSharper disable InconsistentNaming + +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs index a780300..0d76f1c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs @@ -1,4 +1,6 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs index 5341b31..9fd3aed 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs index 426c999..719f49b 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +// ReSharper disable InconsistentNaming + +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs index ea156f0..ca2e376 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs @@ -37,7 +37,7 @@ public async Task TestOnActionExecutionAsync() Parameter2 = "Value 2", Parameter3 = "Value 3" } - }, + } }; var controllerActionDescriptor = new ControllerActionDescriptor { @@ -129,7 +129,7 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT { Parameters = [ - new() + new ParameterDescriptor { Name = "request", ParameterType = typeof(CreateAnimalRequest), @@ -169,46 +169,35 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT // Assert var modelStateDictionaryValues = modelStateDictionary.Values.ToList(); var validationFailuresValues = validationFailures.Values.ToList(); - var badRequestObjectResult = (BadRequestObjectResult)actionExecutingContext.Result!; - var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails)badRequestObjectResult.Value!; + var badRequestObjectResult = (BadRequestObjectResult) actionExecutingContext.Result!; + var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails) badRequestObjectResult.Value!; Assert.Contains(validationFailuresValues[0].First(), modelStateDictionaryValues[0].Errors.Select(error => error.ErrorMessage)); Assert.Contains(validationFailuresValues[0].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(CreatePersonRequest.Name)][0]); } - public class AnimalsController : ControllerBase - { - } + public class AnimalsController : ControllerBase; - public class CreateAnimalRequest - { - } + public class CreateAnimalRequest; public class CreatePersonRequest : CreateAnimalRequest { public required string Name { get; set; } } - public class CreateAnimalRequestValidator : AbstractValidator - { - public CreateAnimalRequestValidator() - { - } - } + public class CreateAnimalRequestValidator : AbstractValidator; public class CreatePersonRequestValidator : AbstractValidator { public CreatePersonRequestValidator() { - this.Include(new CreateAnimalRequestValidator()); + Include(new CreateAnimalRequestValidator()); - this.RuleFor(x => x.Name).Equal("John Doe"); + RuleFor(x => x.Name).Equal("John Doe"); } } - public class TestController : ControllerBase - { - } + public class TestController : ControllerBase; private class TestModel { diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs index 4f264a0..48056c9 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -14,9 +16,9 @@ public class FluentValidationAutoValidationDefaultResultFactoryTest { private static readonly Dictionary ValidationFailures = new() { - {"Property 1", new[] {"Error message 1"}}, - {"Property 2", new[] {"Error message 2"}}, - {"Property 3", new[] {"Error message 3"}}, + {"Property 1", ["Error message 1"]}, + {"Property 2", ["Error message 2"]}, + {"Property 3", ["Error message 3"]}, }; [Fact] diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs index e455f75..dd60a3c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs @@ -1,4 +1,6 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; using FluentValidation; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs index bfbd3d6..d122506 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs @@ -1,4 +1,6 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; using Microsoft.AspNetCore.Mvc; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs index 481193e..f5789c7 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; From 6e87e769eb3e360baecabc6b1cfc64540706a206 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:21:48 -0400 Subject: [PATCH 03/13] support struct model types (#75) --- .editorconfig | 158 ++++++++++++++++++ .../src/Extensions/TypeExtensions.cs | 18 +- .../Extensions/TypeExtensionsTest.cs | 5 + 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c4bed6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,158 @@ +[*] +charset = utf-8-bom +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 +max_line_length = 960 + +[*.yml] +indent_size = 2 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_space_after_cast = true +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.interfaces_rule.import_to_resharper = True +dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces +dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7 +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = True +dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private) +dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private) +dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private) +dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = True +dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private) +dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3 +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper = True +dotnet_naming_rule.type_parameters_rule.resharper_description = Type parameters +dotnet_naming_rule.type_parameters_rule.resharper_guid = 2c62818f-621b-4425-adc9-78611099bfcb +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix = I +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix = T +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds = constant_field +dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field,readonly_field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = readonly,static +dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field +dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_autodetect_indent_settings = true +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_accessor_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false +resharper_space_within_single_line_array_initializer_braces = false +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_mvc_action_not_resolved_highlighting = warning +resharper_mvc_area_not_resolved_highlighting = warning +resharper_mvc_controller_not_resolved_highlighting = warning +resharper_mvc_masterpage_not_resolved_highlighting = warning +resharper_mvc_partial_view_not_resolved_highlighting = warning +resharper_mvc_template_not_resolved_highlighting = warning +resharper_mvc_view_component_not_resolved_highlighting = warning +resharper_mvc_view_component_view_not_resolved_highlighting = warning +resharper_mvc_view_not_resolved_highlighting = warning +resharper_razor_assembly_not_resolved_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_style = space +indent_size = 2 + +[*.scss] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs index f8b08ca..3d406e2 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions @@ -7,18 +8,31 @@ public static class TypeExtensions { public static bool IsCustomType(this Type? type) { - var builtInTypes = new[] + if (type == null || type.IsEnum || type.IsPrimitive) + { + return false; + } + + var builtInTypes = new HashSet { typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), + typeof(DateOnly), + typeof(TimeOnly), + typeof(Uri), typeof(Guid), typeof(Enum) }; - return type != null && type.IsClass && !type.IsEnum && !type.IsValueType && !type.IsPrimitive && !builtInTypes.Contains(type); + if (builtInTypes.Contains(type)) + { + return false; + } + + return type.IsClass || type.IsValueType; } public static bool HasCustomAttribute(this Type type) where TAttribute : Attribute diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs index d122506..37df0f3 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs @@ -5,6 +5,7 @@ using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; using Xunit; +using Xunit.Abstractions; namespace SharpGrip.FluentValidation.AutoValidation.Tests.FluentValidation.AutoValidation.Shared.Extensions; @@ -15,6 +16,7 @@ public void Test_IsCustomType() { Assert.True(typeof(TestModelClass).IsCustomType()); Assert.True(typeof(TestModelRecord).IsCustomType()); + Assert.True(typeof(TestModelStruct).IsCustomType()); Assert.False(typeof(TestModelEnum).IsCustomType()); Assert.False(typeof(Enum).IsCustomType()); Assert.False(typeof(string).IsCustomType()); @@ -38,6 +40,7 @@ public void Test_IsCustomType() Assert.False(typeof(Guid).IsCustomType()); Assert.False(typeof(DateOnly).IsCustomType()); Assert.False(typeof(TimeOnly).IsCustomType()); + Assert.False(typeof(Uri).IsCustomType()); } [Fact] @@ -70,6 +73,8 @@ private class TestModelClass; [AutoValidateNever] private record TestModelRecord; + private struct TestModelStruct; + private enum TestModelEnum; private class TestInherits1 : Controller; From a4a2a0d705294b85a2a392224d14ffb17f036118 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Thu, 13 Nov 2025 21:49:52 -0400 Subject: [PATCH 04/13] code cleanup --- Directory.Build.props | 1 - .../src/Extensions/ServiceCollectionExtensions.cs | 2 +- .../Extensions/TypeExtensionsTest.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b7ebf93..7e6f786 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,6 @@ net8.0;net9.0;net10.0 enable - 8.0 NU1701 true true diff --git a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs index 9f2cc79..5009139 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs @@ -51,7 +51,7 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService } // Create a default instance of the `ModelStateInvalidFilter` to access the non static property `Order` in a static context. - var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions {InvalidModelStateResponseFactory = context => new OkResult()}, NullLogger.Instance); + var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions {InvalidModelStateResponseFactory = _ => new OkResult()}, NullLogger.Instance); // Make sure we insert the `FluentValidationAutoValidationActionFilter` before the built-in `ModelStateInvalidFilter` to prevent it short-circuiting the request. serviceCollection.Configure(options => options.Filters.Add(modelStateInvalidFilter.Order - 1)); diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs index 37df0f3..3920f6c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs @@ -5,7 +5,6 @@ using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; using Xunit; -using Xunit.Abstractions; namespace SharpGrip.FluentValidation.AutoValidation.Tests.FluentValidation.AutoValidation.Shared.Extensions; From cac8384d7d472212374a7c6aeb87960987d78254 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:22:04 -0400 Subject: [PATCH 05/13] 74 allow validation of the struct dto or explicitly document unsupported types (#77) --- .../src/Filters/FluentValidationAutoValidationActionFilter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index a3e2c16..c5e888a 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -58,9 +58,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var hasAutoValidateAlwaysAttribute = parameterInfo?.HasCustomAttribute() ?? false; var hasAutoValidateNeverAttribute = parameterInfo?.HasCustomAttribute() ?? false; - if (subject != null && parameterType != null && parameterType.IsCustomType() && - !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && - serviceProvider.GetValidator(parameterType) is IValidator validator) + if (subject != null && parameterType != null && parameterType.IsCustomType() && !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && serviceProvider.GetValidator(parameterType) is IValidator validator) { // ReSharper disable once SuspiciousTypeConversion.Global var validatorInterceptor = validator as IValidatorInterceptor; From eed287b1a37dc2048434ce300bf52713b2ba5e13 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Tue, 3 Feb 2026 20:23:46 -0400 Subject: [PATCH 06/13] package updates --- .../FluentValidation.AutoValidation.Endpoints.csproj | 2 +- .../FluentValidation.AutoValidation.Mvc.csproj | 2 +- .../FluentValidation.AutoValidation.Shared.csproj | 2 +- Tests/FluentValidation.AutoValidation.Tests.csproj | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj index 44b56d1..4eb4038 100644 --- a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj +++ b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj index 2b39e94..5dd67ba 100644 --- a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj +++ b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj index d7cf986..2ed14a7 100644 --- a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj +++ b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/FluentValidation.AutoValidation.Tests.csproj b/Tests/FluentValidation.AutoValidation.Tests.csproj index e549e39..8f03e9b 100644 --- a/Tests/FluentValidation.AutoValidation.Tests.csproj +++ b/Tests/FluentValidation.AutoValidation.Tests.csproj @@ -7,7 +7,7 @@ - + all @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 741e05c8dcc09b46e6a88a0949978f34d336d00d Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:40:46 -0400 Subject: [PATCH 07/13] improve interceptorsa and result factories (#78) --- ...tValidationAutoValidationEndpointFilter.cs | 9 ++-- .../IGlobalValidationInterceptor.cs | 28 +++++++++-- .../src/Interceptors/IValidatorInterceptor.cs | 4 +- ...entValidationAutoValidationActionFilter.cs | 20 ++++---- .../IGlobalValidationInterceptor.cs | 28 +++++++++-- .../src/Interceptors/IValidatorInterceptor.cs | 4 +- ...ationAutoValidationDefaultResultFactory.cs | 10 ++-- ...ntValidationAutoValidationResultFactory.cs | 11 +++-- README.md | 49 ++++++++++--------- ...idationAutoValidationEndpointFilterTest.cs | 17 ++++--- .../AutoValidationMvcConfigurationTest.cs | 10 ++-- .../ServiceCollectionExtensionsTest.cs | 8 ++- ...alidationAutoValidationActionFilterTest.cs | 21 ++++---- ...nAutoValidationDefaultResultFactoryTest.cs | 10 ++-- UPGRADING.md | 41 ++++++++++++++++ 15 files changed, 189 insertions(+), 81 deletions(-) create mode 100644 UPGRADING.md diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index 06fdf94..82bb295 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -18,7 +18,6 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter { if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) { - // ReSharper disable once SuspiciousTypeConversion.Global var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); @@ -26,24 +25,24 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter if (validatorInterceptor != null) { - validationContext = validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext) ?? validationContext; + validationContext = await validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } if (globalValidationInterceptor != null) { - validationContext = globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext) ?? validationContext; + validationContext = await globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } var validationResult = await validator.ValidateAsync(validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted); if (validatorInterceptor != null) { - validationResult = validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext) ?? validationResult; + validationResult = await validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (globalValidationInterceptor != null) { - validationResult = globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext) ?? validationResult; + validationResult = await globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (!validationResult.IsValid) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs index 6cc2cd7..13cbc6a 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Http; @@ -11,7 +13,27 @@ namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors /// public interface IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); + /// + /// Executes custom logic before the validation process. Allows intercepting and altering the validation context prior to the execution of the validation rules. + /// + /// The context of the currently executing endpoint filter, providing access to details about the HTTP request and endpoint. + /// The validation context containing information about the object being validated. + /// A token to monitor for cancellation requests. + /// + /// A transformed or new instance to be used in the validation process, or null if no changes need to be applied. + /// + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + + /// + /// Executes custom logic after the validation process. Allows intercepting and altering the validation result or performing additional operations after the validation has been completed. + /// + /// The context of the currently executing endpoint filter, providing access to details about the HTTP request and endpoint. + /// The validation context containing information about the object that was validated. + /// The result of the validation process, including validation errors if any exist. + /// A token to monitor for cancellation requests. + /// + /// A modified or new instance, or null if no changes are required to the validation result. + /// + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs index 28c70e1..0896c1f 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs @@ -5,7 +5,5 @@ /// /// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated. /// - public interface IValidatorInterceptor : IGlobalValidationInterceptor - { - } + public interface IValidatorInterceptor : IGlobalValidationInterceptor; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index c5e888a..1dfccd2 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -47,6 +49,8 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC return; } + var validationResults = new Dictionary(); + foreach (var parameter in controllerActionDescriptor.Parameters) { if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject)) @@ -68,24 +72,25 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (validatorInterceptor != null) { - validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + validationContext = await validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } if (globalValidationInterceptor != null) { - validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + validationContext = await globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted); + validationResults.Add(validationContext, validationResult); if (validatorInterceptor != null) { - validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + validationResult = await validatorInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (globalValidationInterceptor != null) { - validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + validationResult = await globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (!validationResult.IsValid) @@ -106,7 +111,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var problemDetailsFactory = serviceProvider.GetRequiredService(); var validationProblemDetails = problemDetailsFactory.CreateValidationProblemDetails(actionExecutingContext.HttpContext, actionExecutingContext.ModelState); - actionExecutingContext.Result = fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails); + actionExecutingContext.Result = await fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults); return; } @@ -124,10 +129,7 @@ private bool IsValidController(object controller) return false; } - return controller is ControllerBase || - controllerType.HasCustomAttribute() || - controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || - controllerType.InheritsFromTypeWithNameEndingIn("Controller"); + return controller is ControllerBase || controllerType.HasCustomAttribute() || controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || controllerType.InheritsFromTypeWithNameEndingIn("Controller"); } private bool HasValidBindingSource(BindingSource? bindingSource) diff --git a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs index 8c88e59..b4197f1 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Mvc.Filters; @@ -11,7 +13,27 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors /// public interface IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); + /// + /// Executes custom logic before the validation process. Allows intercepting and altering the validation context prior to the execution of the validation rules. + /// + /// The context of the currently executing action, providing access to details about the HTTP request and action. + /// The validation context containing information about the object being validated. + /// A token to monitor for cancellation requests. + /// + /// A transformed or new instance to be used in the validation process, or null if no changes need to be applied. + /// + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + + /// + /// Executes custom logic after the validation process. Allows intercepting and altering the validation result or performing additional operations after the validation has been completed. + /// + /// The context of the currently executing action, providing access to details about the HTTP request and action. + /// The validation context containing information about the object that was validated. + /// The result of the validation process, including validation errors if any exist. + /// A token to monitor for cancellation requests. + /// + /// A modified or new instance, or null if no changes are required to the validation result. + /// + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs index 92d8d97..3414d5f 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs @@ -5,7 +5,5 @@ /// /// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated. /// - public interface IValidatorInterceptor : IGlobalValidationInterceptor - { - } + public interface IValidatorInterceptor : IGlobalValidationInterceptor; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs b/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs index bef32b9..9a6a899 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Results { public class FluentValidationAutoValidationDefaultResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new BadRequestObjectResult(validationProblemDetails); + return Task.FromResult(new BadRequestObjectResult(validationProblemDetails)); } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs b/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs index a106f1a..42da4ab 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Results @@ -10,7 +14,8 @@ public interface IFluentValidationAutoValidationResultFactory /// /// The associated with the current request/response. /// The instance object containing the validation failures. - /// The object to be executed by the controller action. - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails); + /// The dictionary of instances keyed by the models containing the validation results from all validators that were executed. + /// The object to be executed by the controller action or null to prevent short-circuiting the action. + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults); } } \ No newline at end of file diff --git a/README.md b/README.md index cf29fe7..9de90bb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=SharpGrip_FluentValidation.AutoValidation&metric=security_rating)](https://sonarcloud.io/summary/overall?id=SharpGrip_FluentValidation.AutoValidation) \ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=SharpGrip_FluentValidation.AutoValidation&metric=coverage)](https://sonarcloud.io/summary/overall?id=SharpGrip_FluentValidation.AutoValidation) +## Upgrading +Check out the [upgrade guide](UPGRADING.md). + ## Introduction SharpGrip FluentValidation AutoValidation is an extension of the [FluentValidation](https://github.com/FluentValidation/FluentValidation) (v10+) library enabling automatic asynchronous validation in MVC controllers and minimal APIs (endpoints). @@ -116,7 +119,7 @@ builder.Services.AddFluentValidationAutoValidation(configuration => public class CustomResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { return new BadRequestObjectResult(new {Title = "Validation errors", ValidationErrors = validationProblemDetails?.Errors}); } @@ -171,13 +174,13 @@ Implement the `IValidatorInterceptor` interface directly on a specific validator In the validation process, both the global and the validator interceptors are resolved and invoked (if they exist), thereby establishing a miniature pipeline of validation interceptors: ``` -==> IValidatorInterceptor.BeforeValidation() -==> IGlobalValidationInterceptor.BeforeValidation() +IValidatorInterceptor.BeforeValidation() +IGlobalValidationInterceptor.BeforeValidation() -Validation +>> Validation << -==> IValidatorInterceptor.AfterValidation() -==> IGlobalValidationInterceptor.AfterValidation() +IValidatorInterceptor.AfterValidation() +IGlobalValidationInterceptor.AfterValidation() ``` Both interfaces define a `BeforeValidation` and a `AfterValidation` method. @@ -186,7 +189,7 @@ The `BeforeValidation` method gets called before validation and allows you to re In case you return `null` the default `IValidationContext` will be passed to the validator. The `AfterValidation` method gets called after validation and allows you to return a custom `IValidationResult` which gets passed to the `IFluentValidationAutoValidationResultFactory`. -In case you return `null` the default `IValidationResult` will be passed to the `IFluentValidationAutoValidationResultFactory`. +In case you return `null` the existing `ValidationResult` will be passed to the `IFluentValidationAutoValidationResultFactory`. ### MVC controllers @@ -196,16 +199,16 @@ builder.Services.AddTransient BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } @@ -219,16 +222,16 @@ private class TestValidator : AbstractValidator, IValidatorIntercepto RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } ``` @@ -241,16 +244,16 @@ builder.Services.AddTransient BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } @@ -264,16 +267,16 @@ private class TestValidator : AbstractValidator, IValidatorIntercepto RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } ``` diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs index 0d76f1c..44a3321 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; @@ -81,27 +82,27 @@ public TestValidator() RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } private class GlobalValidationInterceptor : IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs index 2df849c..2ffc6de 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; using SharpGrip.FluentValidation.AutoValidation.Mvc.Results; @@ -24,9 +28,9 @@ public void TestOverrideDefaultResultFactoryWith() private class TestResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new OkResult(); + return Task.FromResult(new OkResult()); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs index 719f49b..532d583 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,5 +1,9 @@ // ReSharper disable InconsistentNaming +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -88,9 +92,9 @@ private static void AssertNotContainsServiceDescriptor CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new OkResult(); + return Task.FromResult(new OkResult()); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs index ca2e376..18bbba0 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; @@ -76,7 +77,7 @@ public async Task TestOnActionExecutionAsync() serviceProvider.GetService(typeof(ProblemDetailsFactory)).Returns(problemDetailsFactory); problemDetailsFactory.CreateValidationProblemDetails(httpContext, modelStateDictionary).Returns(validationProblemDetails); - fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails)); + fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, Arg.Any>()).Returns(new BadRequestObjectResult(validationProblemDetails)); httpContext.RequestServices.Returns(serviceProvider); actionExecutingContext.Controller.Returns(controller); actionExecutingContext.ActionDescriptor = controllerActionDescriptor; @@ -156,7 +157,7 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT var actionExecutedContext = Substitute.For(actionContext, new List(), new object()); var fluentValidationAutoValidationResultFactory = Substitute.For(); - fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails)); + fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, Arg.Any>()).Returns(new BadRequestObjectResult(validationProblemDetails)); var autoValidationMvcConfiguration = Substitute.For>(); autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); @@ -215,27 +216,27 @@ public TestValidator() RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } private class GlobalValidationInterceptor : IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs index 48056c9..8ab8de6 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs @@ -1,6 +1,9 @@ // ReSharper disable InconsistentNaming using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -22,7 +25,7 @@ public class FluentValidationAutoValidationDefaultResultFactoryTest }; [Fact] - public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuiltInModelValidation_False() + public async Task TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuiltInModelValidation_False() { var fluentValidationAutoValidationDefaultResultFactory = new FluentValidationAutoValidationDefaultResultFactory(); @@ -31,9 +34,10 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuilt var validationProblemDetails = new ValidationProblemDetails(ValidationFailures); var badRequestObjectResult = new BadRequestObjectResult(validationProblemDetails); + var validationResults = new Dictionary(); - var resultFactoryResult = (BadRequestObjectResult) fluentValidationAutoValidationDefaultResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails); + var resultFactoryResult = (BadRequestObjectResult?) await fluentValidationAutoValidationDefaultResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults); - Assert.Equal(badRequestObjectResult.Value, resultFactoryResult.Value); + Assert.Equal(badRequestObjectResult.Value, resultFactoryResult?.Value); } } \ No newline at end of file diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..d1b15de --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,41 @@ +# SharpGrip FluentValidation AutoValidation + +## Upgrade guide + +### Upgrade from v1 to v2 + +#### Result factories - MVC controllers + +```diff +- public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails); ++ public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults); +``` + +#### Validation interceptors - MVC controllers + +```diff +- public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); ++ public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + +- public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); ++ public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); +``` + +#### Validation interceptors - Minimal APIs (endpoints) + +```diff +- public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); ++ public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + +- public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); ++ public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); +``` + +#### Attributes - MVC controllers + +Replace the deprecated `SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.FluentValidationAutoValidationAttribute` with `SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.AutoValidationAttribute`. + +```diff +- [FluentValidationAutoValidation] ++ [AutoValidation] +``` \ No newline at end of file From 7f9b19085433cdb071d93c18effd67b4e4089972 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:56:44 -0400 Subject: [PATCH 08/13] add support for collections (#79) --- .../src/Extensions/TypeExtensions.cs | 8 ++ .../Extensions/TypeExtensionsTest.cs | 82 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs index 3d406e2..019bd3c 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -32,6 +33,13 @@ public static bool IsCustomType(this Type? type) return false; } + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + var underlyingType = type.IsArray ? type.GetElementType() : type.GetGenericArguments().LastOrDefault(); + + return underlyingType != null && underlyingType.IsCustomType(); + } + return type.IsClass || type.IsValueType; } diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs index 3920f6c..d99daa0 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs @@ -1,6 +1,7 @@ // ReSharper disable InconsistentNaming using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; @@ -42,6 +43,87 @@ public void Test_IsCustomType() Assert.False(typeof(Uri).IsCustomType()); } + [Fact] + public void Test_IsCustomType_Collections() + { + Assert.True(typeof(ICollection).IsCustomType()); + Assert.True(typeof(ICollection).IsCustomType()); + Assert.True(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + + Assert.True(typeof(IList).IsCustomType()); + Assert.True(typeof(IList).IsCustomType()); + Assert.True(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + + Assert.True(typeof(List).IsCustomType()); + Assert.True(typeof(List).IsCustomType()); + Assert.True(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + + Assert.True(typeof(TestModelClass[]).IsCustomType()); + Assert.True(typeof(TestModelRecord[]).IsCustomType()); + Assert.True(typeof(TestModelStruct[]).IsCustomType()); + Assert.False(typeof(TestModelEnum[]).IsCustomType()); + Assert.False(typeof(Enum[]).IsCustomType()); + Assert.False(typeof(string[]).IsCustomType()); + + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + + Assert.True(typeof(HashSet).IsCustomType()); + Assert.True(typeof(HashSet).IsCustomType()); + Assert.True(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + + Assert.True(typeof(ISet).IsCustomType()); + Assert.True(typeof(ISet).IsCustomType()); + Assert.True(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + } + [Fact] public void Test_HasCustomAttribute() { From ddc5345d0dba6270a11b9579dae56cb7389781ec Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:00:28 -0400 Subject: [PATCH 09/13] 74 allow validation of the struct dto or explicitly document unsupported types (#80) From 0e12f147d529669ae54c83f41cea13c2dad567d4 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:04:59 -0400 Subject: [PATCH 10/13] add logging (#81) --- .../Extensions/ServiceCollectionExtensions.cs | 3 +- ...tValidationAutoValidationEndpointFilter.cs | 19 ++++++- .../Attributes/AutoValidateAlwaysAttribute.cs | 4 +- .../Attributes/AutoValidateNeverAttribute.cs | 4 +- .../src/Attributes/AutoValidationAttribute.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 6 +- ...entValidationAutoValidationActionFilter.cs | 56 +++++++++++++++---- ...ationAutoValidationObjectModelValidator.cs | 16 +----- ...lidationAutoValidationValidationVisitor.cs | 23 +------- README.md | 23 ++++---- ...idationAutoValidationEndpointFilterTest.cs | 7 ++- ...alidationAutoValidationActionFilterTest.cs | 8 ++- 12 files changed, 95 insertions(+), 78 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs index f4b0a82..5d4250a 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs @@ -14,8 +14,7 @@ public static class ServiceCollectionExtensions /// The service collection. /// The configuration delegate used to configure the FluentValidation AutoValidation Endpoints validation. /// The service collection. - public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection, - Action? autoValidationEndpointsConfiguration = null) + public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection, Action? autoValidationEndpointsConfiguration = null) { var configuration = new AutoValidationEndpointsConfiguration(); diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index 82bb295..5b9fa37 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -2,13 +2,14 @@ using FluentValidation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters { - public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter + public class FluentValidationAutoValidationEndpointFilter(ILogger logger) : IEndpointFilter { public async ValueTask InvokeAsync(EndpointFilterInvocationContext endpointFilterInvocationContext, EndpointFilterDelegate next) { @@ -18,6 +19,8 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter { if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) { + logger.LogDebug("Starting validation for argument of type '{Type}'.", argument.GetType().Name); + var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); @@ -25,11 +28,13 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter if (validatorInterceptor != null) { + logger.LogDebug("Invoking validator interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name); validationContext = await validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } if (globalValidationInterceptor != null) { + logger.LogDebug("Invoking global validation interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name); validationContext = await globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } @@ -37,25 +42,37 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter if (validatorInterceptor != null) { + logger.LogDebug("Invoking validator interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name); validationResult = await validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (globalValidationInterceptor != null) { + logger.LogDebug("Invoking global validation interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name); validationResult = await globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (!validationResult.IsValid) { + logger.LogDebug("Validation result not valid for argument '{Argument}': {ErrorCount} validation errors found.", argument.GetType().Name, validationResult.Errors.Count); + var fluentValidationAutoValidationResultFactory = serviceProvider.GetService(); + logger.LogDebug("Creating result for path '{Path}'.", endpointFilterInvocationContext.HttpContext.Request.Path); + if (fluentValidationAutoValidationResultFactory != null) { + logger.LogTrace("Creating result for path '{Path}' using a custom result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + return fluentValidationAutoValidationResultFactory.CreateResult(endpointFilterInvocationContext, validationResult); } + logger.LogTrace("Creating result for path '{Path}' using the default result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(endpointFilterInvocationContext, validationResult); } + + logger.LogDebug("Validation result valid for argument '{Argument}'.", argument.GetType().Name); } } diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs index 173d93d..c866642 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Parameter)] - public class AutoValidateAlwaysAttribute : Attribute - { - } + public class AutoValidateAlwaysAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs index 186e61f..dce6a53 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter)] - public class AutoValidateNeverAttribute : Attribute - { - } + public class AutoValidateNeverAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs index 799ba08..eaaf12c 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class AutoValidationAttribute : Attribute - { - } + public class AutoValidationAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs index 5009139..1235b6b 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs @@ -34,11 +34,7 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService if (configuration.DisableBuiltInModelValidation) { - serviceCollection.AddSingleton(serviceProvider => - new FluentValidationAutoValidationObjectModelValidator( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, - configuration.DisableBuiltInModelValidation)); + serviceCollection.AddSingleton(serviceProvider => new FluentValidationAutoValidationObjectModelValidator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, configuration.DisableBuiltInModelValidation)); } // Add the default result factory. diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 1dfccd2..17714c5 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; @@ -21,27 +22,25 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters { - public class FluentValidationAutoValidationActionFilter : IAsyncActionFilter + public class FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions autoValidationMvcConfiguration, ILogger logger) : IAsyncActionFilter { - private readonly IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory; - private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration; - - public FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions autoValidationMvcConfiguration) - { - this.fluentValidationAutoValidationResultFactory = fluentValidationAutoValidationResultFactory; - this.autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value; - } + private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value; public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingContext, ActionExecutionDelegate next) { + var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; + + logger.LogDebug("Starting validation for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + if (IsValidController(actionExecutingContext.Controller)) { var endpoint = actionExecutingContext.HttpContext.GetEndpoint(); - var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; var serviceProvider = actionExecutingContext.HttpContext.RequestServices; if (endpoint != null && ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && !endpoint.Metadata.OfType().Any()) || endpoint.Metadata.OfType().Any())) { + logger.LogDebug("Skipping validation for action '{Action}' on controller '{Controller}' due to validation strategy or AutoValidateNeverAttribute.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + HandleUnvalidatedEntries(actionExecutingContext); await next(); @@ -64,6 +63,8 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (subject != null && parameterType != null && parameterType.IsCustomType() && !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && serviceProvider.GetValidator(parameterType) is IValidator validator) { + logger.LogDebug("Validating parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + // ReSharper disable once SuspiciousTypeConversion.Global var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); @@ -72,11 +73,13 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (validatorInterceptor != null) { + logger.LogDebug("Invoking validator interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name); validationContext = await validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } if (globalValidationInterceptor != null) { + logger.LogDebug("Invoking global validation interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name); validationContext = await globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } @@ -85,21 +88,31 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (validatorInterceptor != null) { + logger.LogDebug("Invoking validator interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name); validationResult = await validatorInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (globalValidationInterceptor != null) { + logger.LogDebug("Invoking global validation interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name); validationResult = await globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (!validationResult.IsValid) { + logger.LogDebug("Validation result not valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}': {ErrorCount} validation errors found.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName, validationResult.Errors.Count); + foreach (var error in validationResult.Errors) { + logger.LogTrace("Adding validation error '{ErrorMessage}' for '{ParameterName}' to ModelState.", error.ErrorMessage, parameter.Name); + actionExecutingContext.ModelState.AddModelError(error.PropertyName, error.ErrorMessage); } } + else + { + logger.LogDebug("Validation result valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + } } } } @@ -108,13 +121,28 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (!actionExecutingContext.ModelState.IsValid) { + logger.LogDebug("ModelState is not valid for action '{Action}' on controller '{Controller}'. Creating validation problem details.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + var problemDetailsFactory = serviceProvider.GetRequiredService(); var validationProblemDetails = problemDetailsFactory.CreateValidationProblemDetails(actionExecutingContext.HttpContext, actionExecutingContext.ModelState); + logger.LogTrace("Creating action result for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + actionExecutingContext.Result = await fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults); + if (actionExecutingContext.Result != null) + { + logger.LogTrace("Action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + } + else + { + logger.LogTrace("No action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + } + return; } + + logger.LogDebug("ModelState is valid for action '{Action}' on controller '{Controller}'. Proceeding with action execution.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); } await next(); @@ -126,6 +154,8 @@ private bool IsValidController(object controller) if (controllerType.HasCustomAttribute()) { + logger.LogDebug("Controller '{Controller}' is marked with NonControllerAttribute. Skipping validation.", controllerType.Name); + return false; } @@ -147,11 +177,17 @@ private void HandleUnvalidatedEntries(ActionExecutingContext context) { if (autoValidationMvcConfiguration.DisableBuiltInModelValidation) { + logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to true."); + foreach (var modelStateEntry in context.ModelState.Values.Where(modelStateEntry => modelStateEntry.ValidationState == ModelValidationState.Unvalidated)) { modelStateEntry.ValidationState = ModelValidationState.Skipped; } } + else + { + logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to false."); + } } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs index 6c5c6b6..31104f4 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs @@ -5,21 +5,9 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { - public class FluentValidationAutoValidationObjectModelValidator : ObjectModelValidator + public class FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, bool disableBuiltInModelValidation) : ObjectModelValidator(modelMetadataProvider, validatorProviders) { - private readonly bool disableBuiltInModelValidation; - - public FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, bool disableBuiltInModelValidation) - : base(modelMetadataProvider, validatorProviders) - { - this.disableBuiltInModelValidation = disableBuiltInModelValidation; - } - - public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, - IModelValidatorProvider validatorProvider, - ValidatorCache validatorCache, - IModelMetadataProvider metadataProvider, - ValidationStateDictionary? validationState) + public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState) { return new FluentValidationAutoValidationValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState, disableBuiltInModelValidation); } diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index b72f240..22f54ff 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -4,33 +4,12 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { - public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor + public class FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState, bool disableBuiltInModelValidation) : ValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { - private readonly bool disableBuiltInModelValidation; - - public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, - IModelValidatorProvider validatorProvider, - ValidatorCache validatorCache, - IModelMetadataProvider metadataProvider, - ValidationStateDictionary? validationState, - bool disableBuiltInModelValidation) - : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) - { - this.disableBuiltInModelValidation = disableBuiltInModelValidation; - } - - public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel) - { - // If built in model validation is disabled return true for later validation in the action filter. - return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel); - } - -#if !NETCOREAPP3_1 public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? container) { // If built in model validation is disabled return true for later validation in the action filter. return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); } -#endif } } \ No newline at end of file diff --git a/README.md b/README.md index 9de90bb..84be1da 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=SharpGrip_FluentValidation.AutoValidation&metric=coverage)](https://sonarcloud.io/summary/overall?id=SharpGrip_FluentValidation.AutoValidation) ## Upgrading + Check out the [upgrade guide](UPGRADING.md). ## Introduction @@ -75,17 +76,17 @@ app.MapPost("/", (SomeOtherModel someOtherModel) => $"Hello again {someOtherMode ### MVC controllers -| Property | Default value | Description | -|----------------------------------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DisableBuiltInModelValidation | `false` | Disables the built-in .NET model (data annotations) validation. | -| ValidationStrategy | `ValidationStrategy.All` | Configures the validation strategy. Validation strategy `ValidationStrategy.All` enables asynchronous automatic validation on all controllers inheriting from `ControllerBase`. Validation strategy `ValidationStrategy.Annotations` enables asynchronous automatic validation on controllers inheriting from `ControllerBase` decorated (class or method) with a `[AutoValidationAttribute]` attribute. | -| EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Body` binding sources (typically parameters decorated with the `[FromBody]` attribute). | -| EnableFormBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | -| EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Query` binding sources (typically parameters decorated with the `[FromQuery]` attribute). | -| EnablePathBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | -| EnableHeaderBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Header` binding sources (typically parameters decorated with the `[FromHeader]` attribute). | -| EnableCustomBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Custom` binding sources. | -| EnableNullBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters not bound from any binding source (typically parameters without a declared or inferred binding source). | +| Property | Default value | Description | +|----------------------------------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DisableBuiltInModelValidation | `false` | Disables the built-in .NET model (data annotations) validation. | +| ValidationStrategy | `ValidationStrategy.All` | Configures the validation strategy. Validation strategy `ValidationStrategy.All` enables asynchronous automatic validation on all controllers. Validation strategy `ValidationStrategy.Annotations` enables asynchronous automatic validation on controllers decorated (class or method) with a `[AutoValidationAttribute]` attribute. | +| EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Body` binding sources (typically parameters decorated with the `[FromBody]` attribute). | +| EnableFormBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | +| EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Query` binding sources (typically parameters decorated with the `[FromQuery]` attribute). | +| EnablePathBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | +| EnableHeaderBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Header` binding sources (typically parameters decorated with the `[FromHeader]` attribute). | +| EnableCustomBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Custom` binding sources. | +| EnableNullBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters not bound from any binding source (typically parameters without a declared or inferred binding source). | ```cs using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs index 44a3321..1836bab 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs @@ -9,6 +9,7 @@ using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Logging; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors; @@ -28,6 +29,7 @@ public class FluentValidationAutoValidationEndpointFilterTest [Fact] public async Task TestInvokeAsync_ValidatorFound() { + var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); @@ -38,7 +40,7 @@ public async Task TestInvokeAsync_ValidatorFound() var validationFailuresValues = ValidationFailures.Values.ToList(); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); var result = (ValidationProblem) (await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!))!; var problemDetailsErrorValues = result.ProblemDetails.Errors.ToList(); @@ -51,6 +53,7 @@ public async Task TestInvokeAsync_ValidatorFound() [Fact] public async Task TestInvokeAsync_ValidatorNotFound() { + var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); @@ -59,7 +62,7 @@ public async Task TestInvokeAsync_ValidatorNotFound() serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(TestModel))).Returns(null); serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(null); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); var result = await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!); diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs index 18bbba0..93e85c3 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; @@ -66,6 +67,7 @@ public async Task TestOnActionExecutionAsync() var problemDetailsFactory = Substitute.For(); var fluentValidationAutoValidationResultFactory = Substitute.For(); var autoValidationMvcConfiguration = Substitute.For>(); + var logger = Substitute.For>(); var httpContext = Substitute.For(); var controller = Substitute.For(); var actionContext = Substitute.For(httpContext, Substitute.For(), controllerActionDescriptor, modelStateDictionary); @@ -84,7 +86,7 @@ public async Task TestOnActionExecutionAsync() actionExecutingContext.ActionArguments.Returns(actionArguments); autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); - var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration); + var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration, logger); await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext)); @@ -162,7 +164,9 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT var autoValidationMvcConfiguration = Substitute.For>(); autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); - var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration); + var logger = Substitute.For>(); + + var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration, logger); // Act await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext)); From b0fd772bb8cb6da64d6aef24489a4fef33b2b333 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun <51729581+mvdgun@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:05:12 -0400 Subject: [PATCH 11/13] 72 add logging (#82) --- .../FluentValidationAutoValidationEndpointFilter.cs | 2 +- .../FluentValidationAutoValidationActionFilter.cs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index 5b9fa37..8520683 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -54,7 +54,7 @@ public class FluentValidationAutoValidationEndpointFilter(ILogger(); diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 17714c5..98e94a0 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -100,7 +100,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (!validationResult.IsValid) { - logger.LogDebug("Validation result not valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}': {ErrorCount} validation errors found.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName, validationResult.Errors.Count); + logger.LogDebug("Validation result not valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}': {ErrorCount} validation error(s) found.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName, validationResult.Errors.Count); foreach (var error in validationResult.Errors) { @@ -133,13 +133,11 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (actionExecutingContext.Result != null) { logger.LogTrace("Action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); - } - else - { - logger.LogTrace("No action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + + return; } - return; + logger.LogTrace("No action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); } logger.LogDebug("ModelState is valid for action '{Action}' on controller '{Controller}'. Proceeding with action execution.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); From 4d099a834ef3e38afcf5c146eecafd14815a8cdf Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 4 Feb 2026 15:53:36 -0400 Subject: [PATCH 12/13] update default configuration values --- .../src/Configuration/AutoValidationMvcConfiguration.cs | 4 ++-- README.md | 4 ++-- UPGRADING.md | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs index ae56f21..33023ad 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs @@ -30,7 +30,7 @@ public class AutoValidationMvcConfiguration /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromForm] attribute). /// /// - public bool EnableFormBindingSourceAutomaticValidation { get; set; } = false; + public bool EnableFormBindingSourceAutomaticValidation { get; set; } = true; /// /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromQuery] attribute). @@ -42,7 +42,7 @@ public class AutoValidationMvcConfiguration /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromRoute] attribute). /// /// - public bool EnablePathBindingSourceAutomaticValidation { get; set; } = false; + public bool EnablePathBindingSourceAutomaticValidation { get; set; } = true; /// /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromHeader] attribute). diff --git a/README.md b/README.md index 84be1da..a6de430 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,9 @@ app.MapPost("/", (SomeOtherModel someOtherModel) => $"Hello again {someOtherMode | DisableBuiltInModelValidation | `false` | Disables the built-in .NET model (data annotations) validation. | | ValidationStrategy | `ValidationStrategy.All` | Configures the validation strategy. Validation strategy `ValidationStrategy.All` enables asynchronous automatic validation on all controllers. Validation strategy `ValidationStrategy.Annotations` enables asynchronous automatic validation on controllers decorated (class or method) with a `[AutoValidationAttribute]` attribute. | | EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Body` binding sources (typically parameters decorated with the `[FromBody]` attribute). | -| EnableFormBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | +| EnableFormBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | | EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Query` binding sources (typically parameters decorated with the `[FromQuery]` attribute). | -| EnablePathBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | +| EnablePathBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | | EnableHeaderBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Header` binding sources (typically parameters decorated with the `[FromHeader]` attribute). | | EnableCustomBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Custom` binding sources. | | EnableNullBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters not bound from any binding source (typically parameters without a declared or inferred binding source). | diff --git a/UPGRADING.md b/UPGRADING.md index d1b15de..c3d6461 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,6 +4,12 @@ ### Upgrade from v1 to v2 +#### Configuration - MVC controllers +The default values of the configuration options below have been changed: + +- `EnableFormBindingSourceAutomaticValidation` from `false` to `true` +- `EnablePathBindingSourceAutomaticValidation` from `false` to `true` + #### Result factories - MVC controllers ```diff From 9f3eaa8c491dba1a399898bcfaeab4f8d48484b8 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 4 Feb 2026 18:32:51 -0400 Subject: [PATCH 13/13] upgrade actions --- .github/workflows/Build.yaml | 2 +- .github/workflows/Release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index d4d5708..b762f75 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index a46c4f5..68abb60 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0