diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateSpecificAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateSpecificAttribute.cs new file mode 100644 index 0000000..89b109d --- /dev/null +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateSpecificAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class AutoValidateSpecificAttribute : Attribute + { + public string[] RuleSets { get; } + + public AutoValidateSpecificAttribute(params string[] ruleSets) + { + RuleSets = ruleSets; + } + } +} diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index 65408d6..16cbcb1 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using FluentValidation; +using FluentValidation.Internal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -69,7 +70,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); - IValidationContext validationContext = new ValidationContext(subject); + var autoValidateSpecificAttribute = parameterInfo?.GetCustomAttribute(); + + IValidationContext validationContext = ValidationContext + .CreateWithOptions(subject, str => DefineValidationStrategy(str, autoValidateSpecificAttribute?.RuleSets)); if (validatorInterceptor != null) { @@ -155,5 +159,17 @@ private void HandleUnvalidatedEntries(ActionExecutingContext context) } } } + + private void DefineValidationStrategy(ValidationStrategy validationStrategy, string[]? ruleSets) + { + if (ruleSets != null && ruleSets.Length > 0) + { + validationStrategy = validationStrategy.IncludeRuleSets(ruleSets); + return; + } + + validationStrategy = validationStrategy + .IncludeRulesNotInRuleSet(); + } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs index db8fa6f..35fad99 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/ParameterInfoExtensions.cs @@ -10,5 +10,10 @@ public static bool HasCustomAttribute(this ParameterInfo parameterIn { return parameterInfo.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(TAttribute)); } + + public static TAttribute? GetCustomAttribute(this ParameterInfo parameterInfo) where TAttribute : Attribute + { + return parameterInfo.GetCustomAttributes(true).FirstOrDefault(attribute => attribute is TAttribute) as TAttribute; + } } } \ 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 ea156f0..43ea861 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using NSubstitute; +using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; using SharpGrip.FluentValidation.AutoValidation.Mvc.Filters; using SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors; @@ -176,6 +177,89 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT Assert.Contains(validationFailuresValues[0].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(CreatePersonRequest.Name)][0]); } + [Fact] + public async Task TestOnActionExecutionAsync_WithSpecificRuleSets_UseSpecificRuleSets() + { + var actionArguments = new Dictionary + { + { + nameof(TestRuleSetModel), new TestRuleSetModel + { + Parameter1 = "Value 1", + Parameter2 = "Value 2", + Parameter3 = "Value 3", + Parameter4 = null, + } + }, + }; + var controllerActionDescriptor = new ControllerActionDescriptor + { + Parameters = new List + { + new ControllerParameterDescriptor() + { + Name = nameof(TestRuleSetModel), + ParameterType = typeof(TestRuleSetModel), + BindingInfo = new BindingInfo {BindingSource = BindingSource.Body}, + ParameterInfo = typeof(TestRuleSetController) + .GetMethod(nameof(TestRuleSetController.TestAction)) + .GetParameters() + .First(x => x.ParameterType == typeof(TestRuleSetModel)) + } + }, + }; + var validationFailures = new Dictionary + { + [nameof(TestRuleSetModel.Parameter1)] = [$"'{nameof(TestRuleSetModel.Parameter1)}' must be 5 characters in length. You entered 7 characters."], + [nameof(TestRuleSetModel.Parameter2)] = [$"'{nameof(TestRuleSetModel.Parameter2)}' must be 5 characters in length. You entered 7 characters."], + [nameof(TestRuleSetModel.Parameter3)] = [$"'{nameof(TestRuleSetModel.Parameter3)}' must be 5 characters in length. You entered 7 characters."] + }; + + var validationProblemDetails = new ValidationProblemDetails(validationFailures); + var modelStateDictionary = new ModelStateDictionary(); + + var serviceProvider = Substitute.For(); + var problemDetailsFactory = Substitute.For(); + var fluentValidationAutoValidationResultFactory = Substitute.For(); + var autoValidationMvcConfiguration = Substitute.For>(); + var httpContext = Substitute.For(); + var controller = Substitute.For(); + var actionContext = Substitute.For(httpContext, Substitute.For(), controllerActionDescriptor, modelStateDictionary); + var actionExecutingContext = Substitute.For(actionContext, new List(), actionArguments, new object()); + var actionExecutedContext = Substitute.For(actionContext, new List(), new object()); + + serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(TestRuleSetModel))).Returns(new TestRuleSetModelValidator()); + serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(new GlobalValidationInterceptor()); + serviceProvider.GetService(typeof(ProblemDetailsFactory)).Returns(problemDetailsFactory); + + problemDetailsFactory.CreateValidationProblemDetails(httpContext, modelStateDictionary).Returns(validationProblemDetails); + fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails)); + httpContext.RequestServices.Returns(serviceProvider); + actionExecutingContext.Controller.Returns(controller); + actionExecutingContext.ActionDescriptor = controllerActionDescriptor; + actionExecutingContext.ActionArguments.Returns(actionArguments); + autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); + + var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration); + + await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext)); + + var modelStateDictionaryValues = modelStateDictionary.Values.ToList(); + var validationFailuresValues = validationFailures.Values.ToList(); + var badRequestObjectResult = (BadRequestObjectResult)actionExecutingContext.Result!; + var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails)badRequestObjectResult.Value!; + + Assert.Equal(validationFailuresValues.Count, modelStateDictionaryValues.Count); + + Assert.Contains(validationFailuresValues[0].First(), modelStateDictionaryValues[0].Errors.Select(error => error.ErrorMessage)); + Assert.Contains(validationFailuresValues[1].First(), modelStateDictionaryValues[1].Errors.Select(error => error.ErrorMessage)); + Assert.Contains(validationFailuresValues[2].First(), modelStateDictionaryValues[2].Errors.Select(error => error.ErrorMessage)); + + Assert.Contains(validationFailuresValues[0].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(TestRuleSetModel.Parameter1)][0]); + Assert.Contains(validationFailuresValues[1].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(TestRuleSetModel.Parameter2)][0]); + Assert.Contains(validationFailuresValues[2].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(TestRuleSetModel.Parameter3)][0]); + } + public class AnimalsController : ControllerBase { } @@ -249,4 +333,53 @@ private class GlobalValidationInterceptor : IGlobalValidationInterceptor return null; } } + + private class TestRuleSetController : ControllerBase + { + public IActionResult TestAction([AutoValidateSpecific("testRuleSet")] TestRuleSetModel model) + { + return Ok(); + } + } + + private class TestRuleSetModel + { + public string? Parameter1 { get; set; } + public string? Parameter2 { get; set; } + public string? Parameter3 { get; set; } + public string? Parameter4 { get; set; } + } + + private class TestRuleSetModelValidator : AbstractValidator, IValidatorInterceptor + { + public TestRuleSetModelValidator() + { + RuleFor(x => x.Parameter1).Empty(); + RuleFor(x => x.Parameter2).Empty(); + RuleFor(x => x.Parameter3).Empty(); + RuleFor(x => x.Parameter4).Empty(); + + RuleSet("testRuleSet", () => + { + RuleFor(x => x.Parameter1) + .Length(5); + + RuleFor(x => x.Parameter2) + .Length(5); + + RuleFor(x => x.Parameter3) + .Length(5); + }); + } + + public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + { + return null; + } + + public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + { + return null; + } + } } \ No newline at end of file