From cb83c00f062bd0cef49e7d0050d60669a6adadc6 Mon Sep 17 00:00:00 2001 From: Rami Date: Sun, 19 Jan 2025 02:41:35 -0800 Subject: [PATCH 1/3] Added support for calling TryValidateModel Fixes #53 --- .../Extensions/ServiceCollectionExtensions.cs | 14 ++-- ...ationAutoValidationObjectModelValidator.cs | 19 ++++- ...lidationAutoValidationValidationVisitor.cs | 48 +++++++++++-- .../ServiceCollectionExtensionsTest.cs | 6 +- ...nAutoValidationObjectModelValidatorTest.cs | 71 ++++++++++++++++++- ...tionAutoValidationValidationVisitorTest.cs | 13 +++- 6 files changed, 149 insertions(+), 22 deletions(-) diff --git a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs index 9f2cc79..fcdcb24 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs @@ -32,14 +32,12 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService serviceCollection.Configure(autoValidationMvcConfiguration); } - if (configuration.DisableBuiltInModelValidation) - { - serviceCollection.AddSingleton(serviceProvider => - new FluentValidationAutoValidationObjectModelValidator( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, - configuration.DisableBuiltInModelValidation)); - } + serviceCollection.AddSingleton(serviceProvider => + new FluentValidationAutoValidationObjectModelValidator( + serviceProvider, + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, + configuration.DisableBuiltInModelValidation)); // Add the default result factory. serviceCollection.AddScoped(); diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs index 6c5c6b6..a739b89 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -7,11 +8,16 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { public class FluentValidationAutoValidationObjectModelValidator : ObjectModelValidator { + private readonly IServiceProvider serviceProvider; private readonly bool disableBuiltInModelValidation; - public FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, bool disableBuiltInModelValidation) + public FluentValidationAutoValidationObjectModelValidator( + IServiceProvider serviceProvider, + IModelMetadataProvider modelMetadataProvider, + IList validatorProviders, bool disableBuiltInModelValidation) : base(modelMetadataProvider, validatorProviders) { + this.serviceProvider = serviceProvider; this.disableBuiltInModelValidation = disableBuiltInModelValidation; } @@ -21,7 +27,14 @@ public override ValidationVisitor GetValidationVisitor(ActionContext actionConte IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState) { - return new FluentValidationAutoValidationValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState, disableBuiltInModelValidation); + return new FluentValidationAutoValidationValidationVisitor( + serviceProvider, + actionContext, + validatorProvider, + validatorCache, + metadataProvider, + validationState, + disableBuiltInModelValidation); } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index b72f240..a999fc0 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -6,9 +9,12 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor { + private readonly IServiceProvider serviceProvider; private readonly bool disableBuiltInModelValidation; - public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, + public FluentValidationAutoValidationValidationVisitor( + IServiceProvider serviceProvider, + ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, @@ -16,21 +22,55 @@ public FluentValidationAutoValidationValidationVisitor(ActionContext actionConte bool disableBuiltInModelValidation) : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { + this.serviceProvider = serviceProvider; this.disableBuiltInModelValidation = disableBuiltInModelValidation; } public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool 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); + bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel); + return Validate(isBaseValid, key, model); } #if !NETCOREAPP3_1 public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? 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); + bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); + return Validate(isBaseValid, key, model); } #endif + + private bool Validate( + bool isBaseValid, + string? key, + object? model) + { + if (model == null) + { + return isBaseValid; + } + + // Use FluentValidation to perform additional validation + var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); + if (!(this.serviceProvider.GetService(validatorType) is IValidator validator)) + { + return isBaseValid; + } + + var validationResult = validator.Validate(new ValidationContext(model)); + foreach (var error in validationResult.Errors) + { + var keyName = string.IsNullOrEmpty(key) ? error.PropertyName : $"{key}.{error.PropertyName}"; + + if (!ModelState[keyName]?.Errors.Any(e => e.ErrorMessage == error.ErrorMessage) ?? true) + { + ModelState.AddModelError(keyName, error.ErrorMessage); + } + } + + return isBaseValid && validationResult.IsValid; + } } } \ 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 426c999..1e78faa 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs @@ -21,7 +21,7 @@ public void TestAddFluentValidationAutoValidation() AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); - AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); + AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); AssertContainsServiceDescriptor, ConfigureNamedOptions>(serviceCollection, ServiceLifetime.Singleton); } @@ -34,7 +34,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_OverriddenRe AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); - AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); + AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); AssertContainsServiceDescriptor, ConfigureNamedOptions>(serviceCollection, ServiceLifetime.Singleton); } @@ -47,7 +47,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuilt AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Scoped); - AssertNotContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); + AssertContainsServiceDescriptor(serviceCollection, ServiceLifetime.Singleton); AssertContainsServiceDescriptor, ConfigureNamedOptions>(serviceCollection, ServiceLifetime.Singleton); } diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs index 78fe0dc..a1afb33 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs @@ -1,8 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; +using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation; using Xunit; @@ -13,15 +21,74 @@ public class FluentValidationAutoValidationObjectModelValidatorTest [Fact] public void TestGetValidationVisitor() { + var serviceProvider = Substitute.For(); var modelMetadataProvider = Substitute.For(); var modelMetadataProviders = Substitute.For>(); var actionContext = Substitute.For(); var modelValidatorProvider = Substitute.For(); var validatorCache = Substitute.For(); - var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator(modelMetadataProvider, modelMetadataProviders, true); + var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator( + serviceProvider, modelMetadataProvider, modelMetadataProviders, true); Assert.IsType( fluentValidationAutoValidationObjectModelValidator.GetValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null)); } + + [Fact] + public void TryValidateModel_WithInvalidModel_ShouldUpdateModelState() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddControllersWithViews(); + serviceCollection.AddFluentValidationAutoValidation(); + serviceCollection.AddTransient, Test1Controller.Action1ViewModel.Action1ViewModelValidator>(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var viewModel = new Test1Controller.Action1ViewModel + { + ValueMustEqual1 = 0 // Invalid value to trigger validation error + }; + + var httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider + }; + var routeData = Substitute.For(); + var actionDescriptor = Substitute.For(); + var actionContext = new ActionContext(httpContext, routeData, actionDescriptor); + var controller = new Test1Controller + { + ControllerContext = new ControllerContext(actionContext) + }; + + // Act + bool result = controller.TryValidateModel(viewModel); + + // Assert + Assert.False(result); + Assert.False(controller.ModelState.IsValid); + Assert.True(controller.ModelState.ContainsKey(nameof(Test1Controller.Action1ViewModel.ValueMustEqual1))); + var modelError = controller.ModelState[nameof(Test1Controller.Action1ViewModel.ValueMustEqual1)]!.Errors.Single(); + Assert.NotNull(modelError); + Assert.Equal("'ValueMustEqual1' must be equal to '1'.", modelError.ErrorMessage); + } + + public class Test1Controller : Controller + { + public class Action1ViewModel + { + public int ValueMustEqual1 { get; set; } + + internal class Action1ViewModelValidator : AbstractValidator + { + public Action1ViewModelValidator() + { + this.RuleFor(x => x.ValueMustEqual1) + .Equal(1) + .WithName(nameof(ValueMustEqual1)); + } + } + } + } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs index 8efdcdc..de0bddb 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using NSubstitute; @@ -12,13 +13,21 @@ public class FluentValidationAutoValidationValidationVisitorTest [Fact] public void TestGetValidationVisitor() { + var serviceProvider = Substitute.For(); var modelMetadataProvider = Substitute.For(); var actionContext = Substitute.For(); var modelValidatorProvider = Substitute.For(); var validatorCache = Substitute.For(); var fluentValidationAutoValidationObjectModelValidator = - new FluentValidationAutoValidationValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null, true); + new FluentValidationAutoValidationValidationVisitor( + serviceProvider, + actionContext, + modelValidatorProvider, + validatorCache, + modelMetadataProvider, + null, + true); #if NETCOREAPP3_1 Assert.True(fluentValidationAutoValidationObjectModelValidator.Validate(null, null, new TestModel(), true)); From 945cab42044498c17af0326b398c269c42473e6f Mon Sep 17 00:00:00 2001 From: Rami Date: Sun, 19 Jan 2025 23:06:19 -0800 Subject: [PATCH 2/3] Consolidated duplicate code --- .../Extensions/ServiceCollectionExtensions.cs | 1 - ...entValidationAutoValidationActionFilter.cs | 39 ++--------- ...ationAutoValidationObjectModelValidator.cs | 13 ++-- ...lidationAutoValidationValidationVisitor.cs | 44 +++++++----- .../src/Validation/FluentValidationHelper.cs | 70 +++++++++++++++++++ ...nAutoValidationObjectModelValidatorTest.cs | 6 +- ...tionAutoValidationValidationVisitorTest.cs | 12 +++- 7 files changed, 116 insertions(+), 69 deletions(-) create mode 100644 FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationHelper.cs diff --git a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs index fcdcb24..f7aaafd 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs @@ -34,7 +34,6 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService serviceCollection.AddSingleton(serviceProvider => new FluentValidationAutoValidationObjectModelValidator( - serviceProvider, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, configuration.DisableBuiltInModelValidation)); diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 65408d6..0b1a570 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -13,8 +13,8 @@ using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; using SharpGrip.FluentValidation.AutoValidation.Mvc.Enums; -using SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors; using SharpGrip.FluentValidation.AutoValidation.Mvc.Results; +using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters @@ -55,45 +55,16 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject)) { var parameterInfo = (parameter as ControllerParameterDescriptor)?.ParameterInfo; - var parameterType = subject?.GetType(); var bindingSource = parameter.BindingInfo?.BindingSource; 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 (!hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource))) { - // ReSharper disable once SuspiciousTypeConversion.Global - var validatorInterceptor = validator as IValidatorInterceptor; - var globalValidationInterceptor = serviceProvider.GetService(); - - IValidationContext validationContext = new ValidationContext(subject); - - if (validatorInterceptor != null) - { - validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; - } - - if (globalValidationInterceptor != null) - { - validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; - } - - var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted); - - if (validatorInterceptor != null) - { - validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; - } - - if (globalValidationInterceptor != null) - { - validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; - } - - if (!validationResult.IsValid) + var validationResult = await FluentValidationHelper.ValidateWithFluentValidationAsync( + serviceProvider, subject, actionExecutingContext); + if (validationResult != null && !validationResult.IsValid) { foreach (var error in validationResult.Errors) { diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs index a739b89..133b609 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -8,27 +7,25 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { public class FluentValidationAutoValidationObjectModelValidator : ObjectModelValidator { - private readonly IServiceProvider serviceProvider; private readonly bool disableBuiltInModelValidation; public FluentValidationAutoValidationObjectModelValidator( - IServiceProvider serviceProvider, IModelMetadataProvider modelMetadataProvider, - IList validatorProviders, bool disableBuiltInModelValidation) + IList validatorProviders, + bool disableBuiltInModelValidation) : base(modelMetadataProvider, validatorProviders) { - this.serviceProvider = serviceProvider; this.disableBuiltInModelValidation = disableBuiltInModelValidation; } - public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, + public override ValidationVisitor GetValidationVisitor( + ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState) { return new FluentValidationAutoValidationValidationVisitor( - serviceProvider, actionContext, validatorProvider, validatorCache, diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index a999fc0..d31726f 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -1,7 +1,8 @@ -using System; +using System.Collections.Generic; using System.Linq; -using FluentValidation; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -9,11 +10,10 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor { - private readonly IServiceProvider serviceProvider; + private readonly ActionContext actionContext; private readonly bool disableBuiltInModelValidation; public FluentValidationAutoValidationValidationVisitor( - IServiceProvider serviceProvider, ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, @@ -22,7 +22,7 @@ public FluentValidationAutoValidationValidationVisitor( bool disableBuiltInModelValidation) : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { - this.serviceProvider = serviceProvider; + this.actionContext = actionContext; this.disableBuiltInModelValidation = disableBuiltInModelValidation; } @@ -30,7 +30,7 @@ public override bool Validate(ModelMetadata? metadata, string? key, object? mode { // If built in model validation is disabled return true for later validation in the action filter. bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel); - return Validate(isBaseValid, key, model); + return ValidateAsync(isBaseValid, key, model).Result; } #if !NETCOREAPP3_1 @@ -38,39 +38,45 @@ public override bool Validate(ModelMetadata? metadata, string? key, object? mode { // If built in model validation is disabled return true for later validation in the action filter. bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); - return Validate(isBaseValid, key, model); + return ValidateAsync(isBaseValid, key, model).Result; } #endif - private bool Validate( - bool isBaseValid, + private async Task ValidateAsync( + bool defaultValue, string? key, object? model) { if (model == null) { - return isBaseValid; + return defaultValue; } - // Use FluentValidation to perform additional validation - var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType()); - if (!(this.serviceProvider.GetService(validatorType) is IValidator validator)) + var actionExecutingContext = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + null); + + var validationResult = await FluentValidationHelper.ValidateWithFluentValidationAsync( + actionContext.HttpContext.RequestServices, + model, + actionExecutingContext); + if (validationResult == null) { - return isBaseValid; + return defaultValue; } - var validationResult = validator.Validate(new ValidationContext(model)); foreach (var error in validationResult.Errors) { var keyName = string.IsNullOrEmpty(key) ? error.PropertyName : $"{key}.{error.PropertyName}"; - - if (!ModelState[keyName]?.Errors.Any(e => e.ErrorMessage == error.ErrorMessage) ?? true) + if (!this.ModelState[keyName]?.Errors.Any(e => e.ErrorMessage == error.ErrorMessage) ?? true) { - ModelState.AddModelError(keyName, error.ErrorMessage); + this.ModelState.AddModelError(keyName, error.ErrorMessage); } } - return isBaseValid && validationResult.IsValid; + return defaultValue && validationResult.IsValid; } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationHelper.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationHelper.cs new file mode 100644 index 0000000..cf99922 --- /dev/null +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors; +using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; + +namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation +{ + public static class FluentValidationHelper + { + public static async Task ValidateWithFluentValidationAsync( + IServiceProvider serviceProvider, + object? model, + ActionExecutingContext actionExecutingContext) + { + if (model == null) + { + return null; + } + + var modelType = model.GetType(); + if (modelType == null) + { + return null; + } + + if (!modelType.IsCustomType()) + { + return null; + } + + var validator = serviceProvider.GetValidator(modelType) as IValidator; + if (validator == null) + { + return null; + } + + IValidationContext validationContext = new ValidationContext(model); + + var validatorInterceptor = validator as IValidatorInterceptor; + if (validatorInterceptor != null) + { + validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + } + + var globalValidationInterceptor = serviceProvider.GetService(); + if (globalValidationInterceptor != null) + { + validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + } + + var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted); + + if (validatorInterceptor != null) + { + validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + } + + if (globalValidationInterceptor != null) + { + validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + } + + return validationResult; + } + } +} diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs index a1afb33..c376603 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using Microsoft.AspNetCore.Http; @@ -21,7 +20,6 @@ public class FluentValidationAutoValidationObjectModelValidatorTest [Fact] public void TestGetValidationVisitor() { - var serviceProvider = Substitute.For(); var modelMetadataProvider = Substitute.For(); var modelMetadataProviders = Substitute.For>(); var actionContext = Substitute.For(); @@ -29,7 +27,7 @@ public void TestGetValidationVisitor() var validatorCache = Substitute.For(); var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator( - serviceProvider, modelMetadataProvider, modelMetadataProviders, true); + modelMetadataProvider, modelMetadataProviders, true); Assert.IsType( fluentValidationAutoValidationObjectModelValidator.GetValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null)); diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs index de0bddb..bd3c215 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs @@ -1,7 +1,9 @@ -using System; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Routing; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation; using Xunit; @@ -13,15 +15,19 @@ public class FluentValidationAutoValidationValidationVisitorTest [Fact] public void TestGetValidationVisitor() { - var serviceProvider = Substitute.For(); var modelMetadataProvider = Substitute.For(); var actionContext = Substitute.For(); + var httpContext = Substitute.For(); + var routeData = Substitute.For(); + var actionDescriptor = Substitute.For(); + actionContext.HttpContext = httpContext; + actionContext.RouteData = routeData; + actionContext.ActionDescriptor = actionDescriptor; var modelValidatorProvider = Substitute.For(); var validatorCache = Substitute.For(); var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationValidationVisitor( - serviceProvider, actionContext, modelValidatorProvider, validatorCache, From 1daba17fa4eda1eb9ba64f2b10601454baaa85bd Mon Sep 17 00:00:00 2001 From: Rami Date: Fri, 28 Feb 2025 12:25:29 -0800 Subject: [PATCH 3/3] Fixed issue where exceptions in validators would unexpectedly be converted into AggregateException --- Directory.Build.props | 2 +- .../FluentValidationAutoValidationValidationVisitor.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ff8f997..fb1d4b1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ enable - 8.0 + 9.0 NU1701 true true diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index d31726f..08c4412 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -30,7 +30,7 @@ public override bool Validate(ModelMetadata? metadata, string? key, object? mode { // If built in model validation is disabled return true for later validation in the action filter. bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel); - return ValidateAsync(isBaseValid, key, model).Result; + return ValidateAsync(isBaseValid, key, model).GetAwaiter().GetResult(); } #if !NETCOREAPP3_1 @@ -38,7 +38,7 @@ public override bool Validate(ModelMetadata? metadata, string? key, object? mode { // If built in model validation is disabled return true for later validation in the action filter. bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); - return ValidateAsync(isBaseValid, key, model).Result; + return ValidateAsync(isBaseValid, key, model).GetAwaiter().GetResult(); } #endif