From 1db559492b62b6129dda5cfb0411a720f69c3806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:37:03 -0300 Subject: [PATCH 1/9] :sparkles: Creates a configuration to use UseBaseTypeValidations --- .../AutoValidationEndpointsConfiguration.cs | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Configuration/AutoValidationEndpointsConfiguration.cs b/FluentValidation.AutoValidation.Endpoints/src/Configuration/AutoValidationEndpointsConfiguration.cs index 4e82152..d70afcf 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Configuration/AutoValidationEndpointsConfiguration.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Configuration/AutoValidationEndpointsConfiguration.cs @@ -2,24 +2,41 @@ using Microsoft.AspNetCore.Http.HttpResults; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results; -namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration +namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration; + +public class AutoValidationEndpointsConfiguration { - public class AutoValidationEndpointsConfiguration + /// + /// Gets a value indicating whether the validation process should look for validators + /// registered for interfaces or base types when a validator for the concrete type is not found. + /// + public bool UseBaseTypeValidations { get; private set; } + + /// + /// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code. + /// + public Type? OverriddenResultFactory { get; private set; } + + /// + /// Overrides the default result factory with a custom result factory. Custom result factories are required to implement . + /// The default result factory returns the validation errors wrapped in a object. + /// + /// + /// The custom result factory implementing . + public AutoValidationEndpointsConfiguration OverrideDefaultResultFactoryWith() where TResultFactory : IFluentValidationAutoValidationResultFactory { - /// - /// Holds the overridden result factory. This property is meant for infrastructure and should not be used by application code. - /// - public Type? OverriddenResultFactory { get; private set; } + OverriddenResultFactory = typeof(TResultFactory); + return this; + } - /// - /// Overrides the default result factory with a custom result factory. Custom result factories are required to implement . - /// The default result factory returns the validation errors wrapped in a object. - /// - /// - /// The custom result factory implementing . - public void OverrideDefaultResultFactoryWith() where TResultFactory : IFluentValidationAutoValidationResultFactory - { - OverriddenResultFactory = typeof(TResultFactory); - } + /// + /// Enables the fallback mechanism to search for validators in the type hierarchy (interfaces and base classes) + /// if no specific validator is registered for the primary parameter type. + /// + /// The current instance for fluent chaining. + public AutoValidationEndpointsConfiguration WithBaseTypeValidations() + { + UseBaseTypeValidations = true; + return this; } } \ No newline at end of file From e0ef77e4ccbede30d65858c462750d454cf580d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:37:54 -0300 Subject: [PATCH 2/9] :sparkles: Change the GetValidator method to respect the configuration. --- .../Extensions/ServiceProviderExtensions.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs index b15530f..93fca6c 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs @@ -1,13 +1,46 @@ using System; using FluentValidation; -namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions +namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; + +public static class ServiceProviderExtensions { - public static class ServiceProviderExtensions + public static object? GetValidator(this IServiceProvider serviceProvider, Type type, bool useBaseTypeValidations) + { + var validator = serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(type)); + if (validator is not null) return validator; + if (!useBaseTypeValidations) return null; + + return GetValidatorFromBaseClasses(serviceProvider, type) + ?? GetValidatorFromInterfaces(serviceProvider, type); + } + + private static object? GetValidatorFromBaseClasses(IServiceProvider serviceProvider, Type type) { - public static object? GetValidator(this IServiceProvider serviceProvider, Type type) + var baseType = type.BaseType; + while (baseType is not null && baseType != typeof(object)) { - return serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(type)); + var baseValidatorType = typeof(IValidator<>).MakeGenericType(baseType); + var baseValidator = serviceProvider.GetService(baseValidatorType); + + if (baseValidator is not null) return baseValidator; + + baseType = baseType.BaseType; } + + return null; + } + + private static object? GetValidatorFromInterfaces(IServiceProvider serviceProvider, Type type) + { + foreach (var interfaceType in type.GetInterfaces()) + { + var interfaceValidatorType = typeof(IValidator<>).MakeGenericType(interfaceType); + var interfaceValidator = serviceProvider.GetService(interfaceValidatorType); + + if (interfaceValidator is not null) return interfaceValidator; + } + + return null; } } \ No newline at end of file From 7e493a58683f1150c7f54c590cd6427cb1b506c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:43:33 -0300 Subject: [PATCH 3/9] :white_check_mark: Create test for ServiceProviderExtensions.cs --- .../ServiceProviderExtensionsTest.cs | 21 +++++++++++++++- Tests/src/Shared/Stubs/CreatePostRequest.cs | 24 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/src/Shared/Stubs/CreatePostRequest.cs diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs index dd60a3c..811e7d3 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs @@ -4,6 +4,7 @@ using FluentValidation; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; +using SharpGrip.FluentValidation.AutoValidation.Tests.Shared.Stubs; using Xunit; namespace SharpGrip.FluentValidation.AutoValidation.Tests.FluentValidation.AutoValidation.Shared.Extensions; @@ -20,11 +21,29 @@ public void Test_GetValidator() serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(testModel.GetType())).Returns(testModelValidator); - var validator = serviceProvider.GetValidator(testModel.GetType()); + var validator = serviceProvider.GetValidator(testModel.GetType(), false); Assert.Equal(testModelValidator, validator); } + [Fact] + public void Test_GetValidator_WithBaseTypeValidator() + { + var serviceProvider = Substitute.For(); + + var testModel = new CreatePostRequest(); + var testModelValidator = new CreatePostValidator(); + + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(testModel.GetType())).Returns(null); + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(ICreatePost))).Returns(testModelValidator); + + var validator = serviceProvider.GetValidator(testModel.GetType(), false); + Assert.Null(validator); + + validator = serviceProvider.GetValidator(testModel.GetType(), true); + Assert.Equal(testModelValidator, validator); + } + private class TestModel; private class TestModelValidator; diff --git a/Tests/src/Shared/Stubs/CreatePostRequest.cs b/Tests/src/Shared/Stubs/CreatePostRequest.cs new file mode 100644 index 0000000..7228be5 --- /dev/null +++ b/Tests/src/Shared/Stubs/CreatePostRequest.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace SharpGrip.FluentValidation.AutoValidation.Tests.Shared.Stubs; + +public interface ICreatePost +{ + string Body { get; } + string Title { get; } +} + +public class CreatePostRequest : ICreatePost +{ + public string Body { get; set; } = null!; + public string Title { get; set; } = null!; +} + +public class CreatePostValidator : AbstractValidator +{ + public CreatePostValidator() + { + RuleFor(req => req.Title).NotEmpty().WithMessage("Title cannot be null or empty."); + RuleFor(req => req.Body).NotEmpty().WithMessage("Body cannot be null or empty."); + } +} \ No newline at end of file From 2bc67fff5d04d9f25d9ba8d836cb248b39fe13cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:45:52 -0300 Subject: [PATCH 4/9] :t-rex: Do not change the MVC behavior. --- .../src/Filters/FluentValidationAutoValidationActionFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 98e94a0..0d8b6b1 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -61,7 +61,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, false) 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); From a4675d52a2ea13f769634dc1c5c698ea18f204be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:47:20 -0300 Subject: [PATCH 5/9] :recycle: Do code refactor to reduce code complexity and use the new configuration on GetValidator method. --- ...tValidationAutoValidationEndpointFilter.cs | 106 ++++++++++-------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index 8520683..24195e6 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -1,15 +1,19 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration; 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(ILogger logger) : IEndpointFilter + public class FluentValidationAutoValidationEndpointFilter(ILogger logger, IOptions options) : IEndpointFilter { public async ValueTask InvokeAsync(EndpointFilterInvocationContext endpointFilterInvocationContext, EndpointFilterDelegate next) { @@ -17,66 +21,80 @@ public class FluentValidationAutoValidationEndpointFilter(ILogger(); + var validationResult = await ExecuteValidation(endpointFilterInvocationContext, validator, serviceProvider, argument); - IValidationContext validationContext = new ValidationContext(argument); + if (!validationResult.IsValid) return CreateAInvalidResult(endpointFilterInvocationContext, argument, validationResult, serviceProvider); - 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; - } + logger.LogDebug("Validation result valid for argument '{Argument}'.", argument.GetType().Name); + } - 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; - } + return await next(endpointFilterInvocationContext); + } - var validationResult = await validator.ValidateAsync(validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted); + private object CreateAInvalidResult(EndpointFilterInvocationContext endpointFilterInvocationContext, object argument, ValidationResult validationResult, IServiceProvider serviceProvider) + { + logger.LogDebug("Validation result not valid for argument '{Argument}': {ErrorCount} validation error(s) found.", argument.GetType().Name, validationResult.Errors.Count); - 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; - } + var fluentValidationAutoValidationResultFactory = serviceProvider.GetService(); - 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; - } + logger.LogDebug("Creating result for path '{Path}'.", endpointFilterInvocationContext.HttpContext.Request.Path); - if (!validationResult.IsValid) - { - logger.LogDebug("Validation result not valid for argument '{Argument}': {ErrorCount} validation error(s) found.", argument.GetType().Name, validationResult.Errors.Count); + if (fluentValidationAutoValidationResultFactory != null) + { + logger.LogTrace("Creating result for path '{Path}' using a custom result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); - var fluentValidationAutoValidationResultFactory = serviceProvider.GetService(); + return fluentValidationAutoValidationResultFactory.CreateResult(endpointFilterInvocationContext, validationResult); + } - logger.LogDebug("Creating result for path '{Path}'.", endpointFilterInvocationContext.HttpContext.Request.Path); + logger.LogTrace("Creating result for path '{Path}' using the default result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); - if (fluentValidationAutoValidationResultFactory != null) - { - logger.LogTrace("Creating result for path '{Path}' using a custom result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(endpointFilterInvocationContext, validationResult); + } - return fluentValidationAutoValidationResultFactory.CreateResult(endpointFilterInvocationContext, validationResult); - } + private async ValueTask ExecuteValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidator validator, IServiceProvider serviceProvider, object argument) + { + var validatorInterceptor = validator as IValidatorInterceptor; + var globalValidationInterceptor = serviceProvider.GetService(); - logger.LogTrace("Creating result for path '{Path}' using the default result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + IValidationContext validationContext = new ValidationContext(argument); - return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(endpointFilterInvocationContext, validationResult); - } + 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; + } - logger.LogDebug("Validation result valid for argument '{Argument}'.", argument.GetType().Name); - } + 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; } - return await next(endpointFilterInvocationContext); + var validationResult = await validator.ValidateAsync(validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted); + + 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; + } + + return validationResult; } } } \ No newline at end of file From 8b55259eb30c5b2c947c94f959f194c338f2111b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:48:02 -0300 Subject: [PATCH 6/9] :white_check_mark: Implements tests for the new behavior. --- ...idationAutoValidationEndpointFilterTest.cs | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs index 1836bab..5863025 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs @@ -10,9 +10,12 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NSubstitute; +using SharpGrip.FluentValidation.AutoValidation.Endpoints.Configuration; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors; +using SharpGrip.FluentValidation.AutoValidation.Tests.Shared.Stubs; using Xunit; namespace SharpGrip.FluentValidation.AutoValidation.Tests.FluentValidation.AutoValidation.Endpoints.Filters; @@ -32,6 +35,8 @@ public async Task TestInvokeAsync_ValidatorFound() var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); + var configuration = Substitute.For>(); + configuration.Value.Returns(new AutoValidationEndpointsConfiguration()); endpointFilterInvocationContext.HttpContext.Returns(new DefaultHttpContext {RequestServices = serviceProvider}); endpointFilterInvocationContext.Arguments.Returns(new List {new TestModel {Parameter1 = "Value 1", Parameter2 = "Value 2", Parameter3 = "Value 3"}}); @@ -40,7 +45,7 @@ public async Task TestInvokeAsync_ValidatorFound() var validationFailuresValues = ValidationFailures.Values.ToList(); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger, configuration); var result = (ValidationProblem) (await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!))!; var problemDetailsErrorValues = result.ProblemDetails.Errors.ToList(); @@ -56,18 +61,68 @@ public async Task TestInvokeAsync_ValidatorNotFound() var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); + var configuration = Substitute.For>(); + configuration.Value.Returns(new AutoValidationEndpointsConfiguration()); endpointFilterInvocationContext.HttpContext.Returns(new DefaultHttpContext {RequestServices = serviceProvider}); endpointFilterInvocationContext.Arguments.Returns(new List {new TestModel {Parameter1 = "Value 1", Parameter2 = "Value 2", Parameter3 = "Value 3"}}); serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(TestModel))).Returns(null); serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(null); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger, configuration); var result = await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!); Assert.IsType(result); } + + [Fact] + public async Task TestInvokeAsync_BaseTypeValidatorNotFound() + { + var logger = Substitute.For>(); + var serviceProvider = Substitute.For(); + var endpointFilterInvocationContext = Substitute.For(); + var configuration = Substitute.For>(); + configuration.Value.Returns(new AutoValidationEndpointsConfiguration()); + + endpointFilterInvocationContext.HttpContext.Returns(new DefaultHttpContext {RequestServices = serviceProvider}); + endpointFilterInvocationContext.Arguments.Returns(new List {new CreatePostRequest()}); + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(CreatePostRequest))).Returns(null); + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(ICreatePost))).Returns(new CreatePostValidator()); + serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(null); + + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger, configuration); + + var result = await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!); + + Assert.IsType(result); + } + + [Fact] + public async Task TestInvokeAsync_BaseTypeValidatorFound() + { + var logger = Substitute.For>(); + var serviceProvider = Substitute.For(); + var endpointFilterInvocationContext = Substitute.For(); + var configuration = Substitute.For>(); + configuration.Value.Returns(new AutoValidationEndpointsConfiguration().WithBaseTypeValidations()); + + endpointFilterInvocationContext.HttpContext.Returns(new DefaultHttpContext {RequestServices = serviceProvider}); + endpointFilterInvocationContext.Arguments.Returns(new List {new CreatePostRequest()}); + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(CreatePostRequest))).Returns(null); + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(ICreatePost))).Returns(new CreatePostValidator()); + serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(new GlobalValidationInterceptor()); + + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger, configuration); + + var result = await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())); + Assert.IsType(result, false); + var problem = result as ValidationProblem; + var problemDetailsErrorValues = problem.ProblemDetails.Errors.ToList(); + + Assert.Contains(problemDetailsErrorValues, x => x.Value.Contains("Title cannot be null or empty.")); + Assert.Contains(problemDetailsErrorValues, x => x.Value.Contains("Body cannot be null or empty.")); + } private class TestModel { From ece88cb1bef9e6f940af3df0e836ec710c9c3212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:52:28 -0300 Subject: [PATCH 7/9] :white_check_mark: add global culture setup for tests to prevent localized dotnet failures --- Tests/src/Shared/GlobalSetup.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Tests/src/Shared/GlobalSetup.cs diff --git a/Tests/src/Shared/GlobalSetup.cs b/Tests/src/Shared/GlobalSetup.cs new file mode 100644 index 0000000..73bcd64 --- /dev/null +++ b/Tests/src/Shared/GlobalSetup.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +[assembly: Xunit.TestFramework("SharpGrip.FluentValidation.AutoValidation.Tests.Shared.GlobalSetup", "FluentValidation.AutoValidation.Tests")] +namespace SharpGrip.FluentValidation.AutoValidation.Tests.Shared; + +public class GlobalSetup : XunitTestFramework +{ + public GlobalSetup(IMessageSink messageSink) : base(messageSink) + { + SetupEnvironment(); + } + + private static void SetupEnvironment() + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); + } +} \ No newline at end of file From 419baba1bd9273261eacb03c4d938617e86cb907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:53:01 -0300 Subject: [PATCH 8/9] :recycle: refacts the code to improve memory usage. --- .../src/Extensions/TypeExtensions.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs index 019bd3c..ab0ecbc 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -7,6 +7,20 @@ namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions { public static class TypeExtensions { + private static readonly HashSet builtInTypes = + [ + typeof(string), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(DateOnly), + typeof(TimeOnly), + typeof(Uri), + typeof(Guid), + typeof(Enum) + ]; + public static bool IsCustomType(this Type? type) { if (type == null || type.IsEnum || type.IsPrimitive) @@ -14,20 +28,6 @@ public static bool IsCustomType(this Type? type) 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) - }; - if (builtInTypes.Contains(type)) { return false; From ddf4fc299d8fc7b1abd9ab3b577201e5636e0589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20Esb=C3=A9rard?= Date: Sat, 28 Feb 2026 22:54:02 -0300 Subject: [PATCH 9/9] :wrench: add nuget.config to streamline dependency restoration --- nuget.config | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 nuget.config diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..7734148 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + +