diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index ea945a8..690b213 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -20,10 +20,8 @@ public FluentValidationAutoValidationEndpointFilter(IServiceProvider serviceProv public async ValueTask InvokeAsync(EndpointFilterInvocationContext endpointFilterInvocationContext, EndpointFilterDelegate next) { - for (var i = 0; i < endpointFilterInvocationContext.Arguments.Count; i++) + foreach (var argument in endpointFilterInvocationContext.Arguments) { - var argument = endpointFilterInvocationContext.Arguments[i]; - if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) { // ReSharper disable once SuspiciousTypeConversion.Global diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlways.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlways.cs new file mode 100644 index 0000000..880779e --- /dev/null +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlways.cs @@ -0,0 +1,9 @@ +using System; + +namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class AutoValidateAlways : Attribute + { + } +} \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNever.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNever.cs new file mode 100644 index 0000000..bba1d1f --- /dev/null +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNever.cs @@ -0,0 +1,9 @@ +using System; + +namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter)] + public class AutoValidateNever : 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 new file mode 100644 index 0000000..799ba08 --- /dev/null +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class AutoValidationAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs index c90fc32..666edde 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs @@ -2,6 +2,7 @@ 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 { diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index e3fb616..6b6ab19 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -40,8 +40,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var endpoint = actionExecutingContext.HttpContext.GetEndpoint(); var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; - if (autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && - endpoint != null && !endpoint.Metadata.OfType().Any()) + if (endpoint != null && + ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && + !endpoint.Metadata.OfType().Any() && !endpoint.Metadata.OfType().Any()) || + endpoint.Metadata.OfType().Any())) { HandleUnvalidatedEntries(actionExecutingContext); @@ -54,15 +56,14 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC { if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject)) { + var parameterInfo = ((ControllerParameterDescriptor) parameter).ParameterInfo; var parameterType = parameter.ParameterType; var bindingSource = parameter.BindingInfo?.BindingSource; - if (subject != null && parameterType.IsCustomType() && - ((autoValidationMvcConfiguration.EnableBodyBindingSourceAutomaticValidation && bindingSource == BindingSource.Body) || - (autoValidationMvcConfiguration.EnableFormBindingSourceAutomaticValidation && bindingSource == BindingSource.Form) || - (autoValidationMvcConfiguration.EnableQueryBindingSourceAutomaticValidation && bindingSource == BindingSource.Query) || - (autoValidationMvcConfiguration.EnablePathBindingSourceAutomaticValidation && bindingSource == BindingSource.Path) || - (autoValidationMvcConfiguration.EnableCustomBindingSourceAutomaticValidation && bindingSource == BindingSource.Custom))) + var hasAutoValidateAlwaysAttribute = parameterInfo.HasCustomAttribute(); + var hasAutoValidateNeverAttribute = parameterInfo.HasCustomAttribute(); + + if (subject != null && parameterType.IsCustomType() && !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource))) { if (serviceProvider.GetValidator(parameterType) is IValidator validator) { @@ -121,6 +122,15 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC await next(); } + private bool HasValidBindingSource(BindingSource? bindingSource) + { + return (autoValidationMvcConfiguration.EnableBodyBindingSourceAutomaticValidation && bindingSource == BindingSource.Body) || + (autoValidationMvcConfiguration.EnableFormBindingSourceAutomaticValidation && bindingSource == BindingSource.Form) || + (autoValidationMvcConfiguration.EnableQueryBindingSourceAutomaticValidation && bindingSource == BindingSource.Query) || + (autoValidationMvcConfiguration.EnablePathBindingSourceAutomaticValidation && bindingSource == BindingSource.Path) || + (autoValidationMvcConfiguration.EnableCustomBindingSourceAutomaticValidation && bindingSource == BindingSource.Custom); + } + private void HandleUnvalidatedEntries(ActionExecutingContext context) { if (autoValidationMvcConfiguration.DisableBuiltInModelValidation) diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs new file mode 100644 index 0000000..db8fa6f --- /dev/null +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions +{ + public static class ParameterInfoExtensions + { + public static bool HasCustomAttribute(this ParameterInfo parameterInfo) where TAttribute : Attribute + { + return parameterInfo.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(TAttribute)); + } + } +} \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs index a5b55b6..74c7bae 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -19,5 +19,10 @@ public static bool IsCustomType(this Type? type) return type != null && type.IsClass && !type.IsEnum && !type.IsValueType && !type.IsPrimitive && !builtInTypes.Contains(type); } + + public static bool HasCustomAttribute(this Type type) where TAttribute : Attribute + { + return type.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(TAttribute)); + } } } \ No newline at end of file diff --git a/README.md b/README.md index 47d9b4e..1fe8b85 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,9 @@ ## 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). -The library [FluentValidation.AspNetCore](https://github.com/FluentValidation/FluentValidation.AspNetCore) is no longer being maintained and is unsupported. As a result, support for automatic -validation provided by this library is no longer available. -This library re-introduces this functionality for MVC controllers and introduces automatic validation for minimal APIs (endpoints). It enables developers to easily implement automatic validation in -their projects. +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). +The library [FluentValidation.AspNetCore](https://github.com/FluentValidation/FluentValidation.AspNetCore) is no longer being maintained and is unsupported. As a result, support for automatic validation provided by this library is no longer available. +This library re-introduces this functionality for MVC controllers and introduces automatic validation for minimal APIs (endpoints). It enables developers to easily implement automatic validation in their projects. ## Installation @@ -120,14 +117,25 @@ public class CustomResultFactory : IFluentValidationAutoValidationResultFactory public IResult CreateResult(EndpointFilterInvocationContext context, ValidationResult validationResult) { var validationProblemErrors = validationResult.ToValidationProblemErrors(); + return Results.ValidationProblem(validationProblemErrors, "Some details text.", "Some instance text.", (int) HttpStatusCode.BadRequest, "Some title."); } } ``` +## Validation attributes + +### MVC controllers +Customizing automatic validation behavior is achievable through the use of attributes. + +The `[AutoValidateAlways]` attribute can be applied to a controller parameter, compelling automatic validation to disregard the validation check for a valid binding source. +This proves useful when the `ApiBehaviorOptions.SuppressInferBindingSourcesForParameters` option is enabled, and a custom model is used, with parameters bound from multiple binding sources. + +The `[AutoValidateNever]` attribute can be placed on a controller class, controller method, or controller parameter, instructing automatic validation to be skipped. + ## Validation interceptors -Note: Using validation interceptors is considered to be an advanced feature and is not needed for most use cases. +**Note:** Using validation interceptors is considered to be an advanced feature and is not needed for most use cases. Validation interceptors allow you to intercept and alter the validation process by either implementing the `IGlobalValidationInterceptor` interface in a custom class or by implementing the `IValidatorInterceptor` on a single validator. @@ -217,7 +225,6 @@ public class CustomGlobalValidationInterceptor : IValidationInterceptor } } - // Example of a single validator interceptor. private class TestValidator : AbstractValidator, IValidatorInterceptor {