From aa6c0fcb32f4740034bf187d5b0f83a74c25590e Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 4 Feb 2026 09:32:10 -0400 Subject: [PATCH 1/2] improve interceptorsa and result factories --- ...tValidationAutoValidationEndpointFilter.cs | 9 ++-- .../IGlobalValidationInterceptor.cs | 31 ++++++++++-- .../src/Interceptors/IValidatorInterceptor.cs | 4 +- ...entValidationAutoValidationActionFilter.cs | 20 ++++---- .../IGlobalValidationInterceptor.cs | 31 ++++++++++-- .../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, 195 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..6bcce57 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,30 @@ 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..4aa2f1f 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,30 @@ 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 540c634da8919bf82b8da3078ce4f9b9375d00e0 Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Wed, 4 Feb 2026 09:40:32 -0400 Subject: [PATCH 2/2] update docs --- .../src/Interceptors/IGlobalValidationInterceptor.cs | 9 +++------ .../src/Interceptors/IGlobalValidationInterceptor.cs | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs index 6bcce57..13cbc6a 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs @@ -14,21 +14,18 @@ namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors public interface IGlobalValidationInterceptor { /// - /// Executes custom logic before the validation process. Allows intercepting and altering the validation context - /// prior to the execution of the validation rules. + /// 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. + /// 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. + /// 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. diff --git a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs index 4aa2f1f..b4197f1 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs @@ -14,21 +14,18 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors public interface IGlobalValidationInterceptor { /// - /// Executes custom logic before the validation process. Allows intercepting and altering the validation context - /// prior to the execution of the validation rules. + /// 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. + /// 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. + /// 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.