Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add configurable validation for binding sources #8

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>(argument), context.HttpContext.RequestAborted);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +20,21 @@ public class AutoValidationMvcConfiguration
/// </summary>
public ValidationStrategy ValidationStrategy { get; set; } = ValidationStrategy.All;

/// <summary>
/// Enables asynchronous automatic validation for parameters bound from the <see cref="BindingSource.Body"/> binding source (typically parameters decorated with the [FormBody] attribute).
/// </summary>
public bool EnableBodyBindingSourceAutomaticValidation { get; set; } = true;

/// <summary>
/// Enables asynchronous automatic validation for parameters bound from the <see cref="BindingSource.Form"/> binding source (typically parameters decorated with the [FromForm] attribute).
/// </summary>
public bool EnableFormBindingSourceAutomaticValidation { get; set; } = false;

/// <summary>
/// Enables asynchronous automatic validation for parameters bound from the <see cref="BindingSource.Query"/> binding source (typically parameters decorated with the [FormQuery] attribute).
/// </summary>
public bool EnableQueryBindingSourceAutomaticValidation { get; set; } = true;

/// <summary>
/// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
this.autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value;
}

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)

Check warning on line 34 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Refactor this method to reduce its Cognitive Complexity from 43 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 34 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Refactor this method to reduce its Cognitive Complexity from 43 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 34 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Refactor this method to reduce its Cognitive Complexity from 43 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 34 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Refactor this method to reduce its Cognitive Complexity from 43 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 34 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Refactor this method to reduce its Cognitive Complexity from 43 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (context.Controller is ControllerBase controllerBase)
{
Expand All @@ -52,9 +52,12 @@
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)

Check warning on line 60 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)

Check warning on line 60 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)

Check warning on line 60 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)

Check warning on line 60 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)

Check warning on line 60 in FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

View workflow job for this annotation

GitHub Actions / build

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)
{
var validationResult = await validator.ValidateAsync(new ValidationContext<object>(subject), context.HttpContext.RequestAborted);

Expand All @@ -70,6 +73,14 @@
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,8 +8,6 @@ public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor
{
private readonly bool disableBuiltInModelValidation;

private readonly List<Type> systemTypes = Assembly.GetExecutingAssembly().GetType().Module.Assembly.GetExportedTypes().ToList();

public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -68,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<CustomResultFactory>();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -42,6 +40,7 @@ private static void AssertContainsServiceDescriptor<TService, TImplementation>(S
serviceDescriptor.ImplementationInstance?.GetType() == typeof(TImplementation)) &&
serviceDescriptor.Lifetime == serviceLifetime);
}
// ReSharper restore ParameterOnlyUsedForPreconditionCheck.Local

// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
private static void AssertNotContainsServiceDescriptor<TService, TImplementation>(ServiceCollection serviceCollection, ServiceLifetime serviceLifetime)
Expand All @@ -52,6 +51,7 @@ private static void AssertNotContainsServiceDescriptor<TService, TImplementation
serviceDescriptor.ImplementationInstance?.GetType() == typeof(TImplementation)) &&
serviceDescriptor.Lifetime == serviceLifetime);
}
// ReSharper restore ParameterOnlyUsedForPreconditionCheck.Local

private class TestResultFactory : IFluentValidationAutoValidationResultFactory
{
Expand Down