From f3ad8a2bfc7de075111029be4b7260f02b98fed4 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 13 Sep 2023 20:32:23 -0400 Subject: [PATCH 1/3] add configurable validation for binding sources --- ...tValidationAutoValidationEndpointFilter.cs | 2 +- .../AutoValidationMvcConfiguration.cs | 16 +++++++++ ...entValidationAutoValidationActionFilter.cs | 13 +++++++- ...lidationAutoValidationValidationVisitor.cs | 33 +++---------------- .../src/Extensions/TypeExtensions.cs | 23 +++++++++++++ README.md | 11 ++++--- 6 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index b8d79c0..94196c8 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -23,7 +23,7 @@ public FluentValidationAutoValidationEndpointFilter(IServiceProvider serviceProv { var argument = context.Arguments[i]; - if (argument != null && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) + if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) { var validationResult = await validator.ValidateAsync(new ValidationContext(argument), context.HttpContext.RequestAborted); diff --git a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs index 056e341..085ce50 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums; using SharpGrip.FluentValidation.AutoValidation.Mvc.Results; @@ -19,6 +20,21 @@ public class AutoValidationMvcConfiguration /// public ValidationStrategy ValidationStrategy { get; set; } = ValidationStrategy.All; + /// + /// Enables asynchronous automatic validation for parameters bound from the binding source (typically parameters decorated with the [FormBody] attribute). + /// + public bool EnableBodyBindingSourceAutomaticValidation { get; set; } = true; + + /// + /// Enables asynchronous automatic validation for parameters bound from the binding source (typically parameters decorated with the [FromForm] attribute). + /// + public bool EnableFormBindingSourceAutomaticValidation { get; set; } = false; + + /// + /// Enables asynchronous automatic validation for parameters bound from the binding source (typically parameters decorated with the [FormQuery] attribute). + /// + public bool EnableQueryBindingSourceAutomaticValidation { get; set; } = true; + /// /// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code. /// diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 964cce0..abd65c4 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -52,7 +52,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE var parameterType = parameter.ParameterType; var bindingSource = parameter.BindingInfo?.BindingSource; - if (subject != null && (bindingSource == BindingSource.Body || (bindingSource == BindingSource.Query && parameterType.IsClass))) + if (subject != null && parameterType.IsCustomType() && + ((autoValidationMvcConfiguration.EnableBodyBindingSourceAutomaticValidation && bindingSource == BindingSource.Body) || + (autoValidationMvcConfiguration.EnableFormBindingSourceAutomaticValidation && bindingSource == BindingSource.Form) || + (autoValidationMvcConfiguration.EnableQueryBindingSourceAutomaticValidation && bindingSource == BindingSource.Query))) { if (serviceProvider.GetValidator(parameterType) is IValidator validator) { @@ -70,6 +73,14 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE } } + if (autoValidationMvcConfiguration.DisableBuiltInModelValidation) + { + foreach (var modelStateEntry in context.ModelState.Values.Where(modelStateEntry => modelStateEntry.ValidationState == ModelValidationState.Unvalidated)) + { + modelStateEntry.ValidationState = ModelValidationState.Skipped; + } + } + if (!context.ModelState.IsValid) { var validationProblemDetails = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState); diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index f521145..b72f240 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -12,8 +8,6 @@ public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor { private readonly bool disableBuiltInModelValidation; - private readonly List systemTypes = Assembly.GetExecutingAssembly().GetType().Module.Assembly.GetExportedTypes().ToList(); - public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, @@ -27,33 +21,16 @@ public FluentValidationAutoValidationValidationVisitor(ActionContext actionConte public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel) { - // For non-system class types return true for later validation in the action filter. For all other (system) types return the base validation result. - if (IsBuiltInValidationDisabledAndTypeIsClassAndNotSystemType(model)) - { - return true; - } - - return base.Validate(metadata, key, model, 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) { - // For non-system class types return true for later validation in the action filter. For all other (system) types return the base validation result. - if (IsBuiltInValidationDisabledAndTypeIsClassAndNotSystemType(model)) - { - return true; - } - - return base.Validate(metadata, key, model, alwaysValidateAtTopLevel, 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 - - private bool IsBuiltInValidationDisabledAndTypeIsClassAndNotSystemType(object? model) - { - var modelType = model?.GetType(); - - return disableBuiltInModelValidation && modelType != null && modelType.IsClass && !systemTypes.Contains(modelType); - } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..a5b55b6 --- /dev/null +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; + +namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions +{ + public static class TypeExtensions + { + public static bool IsCustomType(this Type? type) + { + var builtInTypes = new[] + { + typeof(string), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid) + }; + + return type != null && type.IsClass && !type.IsEnum && !type.IsValueType && !type.IsPrimitive && !builtInTypes.Contains(type); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index c9e738c..e6c047d 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,13 @@ 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 `FluentValidationAutoValidationAttribute` attribute. | +| 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 `FluentValidationAutoValidationAttribute` attribute. | +| EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from the `BindingSource.Body` binding source (typically parameters decorated with the `[FormBody]` attribute). | +| EnableFormBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from the `BindingSource.Form` binding source (typically parameters decorated with the `[FormForm]` attribute). | +| EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from the `BindingSource.Query` binding source (typically parameters decorated with the `[FormQuery]` attribute). | ``` using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; From 53ca63ecfb7be1614cd579f74f88520145796a8d Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 13 Sep 2023 20:39:39 -0400 Subject: [PATCH 2/3] add new configuration options in the sample code --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index e6c047d..5458c28 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,15 @@ builder.Services.AddFluentValidationAutoValidation(configuration => // Only validate controllers decorated with the `FluentValidationAutoValidation` attribute. configuration.ValidationStrategy = ValidationStrategy.Annotation; + // Enable validation for parameters bound from the `BindingSource.Body` binding source. + configuration.EnableBodyBindingSourceAutomaticValidation = true; + + // Enable validation for parameters bound from the `BindingSource.Form` binding source. + configuration.EnableFormBindingSourceAutomaticValidation = true; + + // Enable validation for parameters bound from the `BindingSource.Query` binding source. + configuration.EnableQueryBindingSourceAutomaticValidation = true; + // Replace the default result factory with a custom implementation. configuration.OverrideDefaultResultFactoryWith(); }); From 16adf5c302523398a06fc169837b27e42657a0fb Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Mon, 18 Sep 2023 14:08:08 -0400 Subject: [PATCH 3/3] cleanup unused usings --- .../src/Extensions/ServiceCollectionExtensionsTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FluentValidation.AutoValidation.Endpoints.Tests/src/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/FluentValidation.AutoValidation.Endpoints.Tests/src/Extensions/ServiceCollectionExtensionsTest.cs index 8e45a73..4d53435 100644 --- a/Tests/FluentValidation.AutoValidation.Endpoints.Tests/src/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/FluentValidation.AutoValidation.Endpoints.Tests/src/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,8 +1,6 @@ using FluentValidation.Results; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results; using Xunit; @@ -42,6 +40,7 @@ private static void AssertContainsServiceDescriptor(S serviceDescriptor.ImplementationInstance?.GetType() == typeof(TImplementation)) && serviceDescriptor.Lifetime == serviceLifetime); } + // ReSharper restore ParameterOnlyUsedForPreconditionCheck.Local // ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local private static void AssertNotContainsServiceDescriptor(ServiceCollection serviceCollection, ServiceLifetime serviceLifetime) @@ -52,6 +51,7 @@ private static void AssertNotContainsServiceDescriptor