Skip to content

Add ability to run specific rule sets #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,7 +70,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC
var validatorInterceptor = validator as IValidatorInterceptor;
var globalValidationInterceptor = serviceProvider.GetService<IGlobalValidationInterceptor>();

IValidationContext validationContext = new ValidationContext<object>(subject);
var autoValidateSpecificAttribute = parameterInfo?.GetCustomAttribute<AutoValidateSpecificAttribute>();

IValidationContext validationContext = ValidationContext<object>
.CreateWithOptions(subject, str => DefineValidationStrategy(str, autoValidateSpecificAttribute?.RuleSets));

if (validatorInterceptor != null)
{
Expand Down Expand Up @@ -155,5 +159,17 @@ private void HandleUnvalidatedEntries(ActionExecutingContext context)
}
}
}

private void DefineValidationStrategy(ValidationStrategy<object> validationStrategy, string[]? ruleSets)
{
if (ruleSets != null && ruleSets.Length > 0)
{
validationStrategy = validationStrategy.IncludeRuleSets(ruleSets);
return;
}

validationStrategy = validationStrategy
.IncludeRulesNotInRuleSet();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ public static bool HasCustomAttribute<TAttribute>(this ParameterInfo parameterIn
{
return parameterInfo.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(TAttribute));
}

public static TAttribute? GetCustomAttribute<TAttribute>(this ParameterInfo parameterInfo) where TAttribute : Attribute
{
return parameterInfo.GetCustomAttributes(true).FirstOrDefault(attribute => attribute is TAttribute) as TAttribute;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, object?>
{
{
nameof(TestRuleSetModel), new TestRuleSetModel
{
Parameter1 = "Value 1",
Parameter2 = "Value 2",
Parameter3 = "Value 3",
Parameter4 = null,
}
},
};
var controllerActionDescriptor = new ControllerActionDescriptor
{
Parameters = new List<ParameterDescriptor>
{
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<string, string[]>
{
[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<IServiceProvider>();
var problemDetailsFactory = Substitute.For<ProblemDetailsFactory>();
var fluentValidationAutoValidationResultFactory = Substitute.For<IFluentValidationAutoValidationResultFactory>();
var autoValidationMvcConfiguration = Substitute.For<IOptions<AutoValidationMvcConfiguration>>();
var httpContext = Substitute.For<HttpContext>();
var controller = Substitute.For<TestController>();
var actionContext = Substitute.For<ActionContext>(httpContext, Substitute.For<RouteData>(), controllerActionDescriptor, modelStateDictionary);
var actionExecutingContext = Substitute.For<ActionExecutingContext>(actionContext, new List<IFilterMetadata>(), actionArguments, new object());
var actionExecutedContext = Substitute.For<ActionExecutedContext>(actionContext, new List<IFilterMetadata>(), 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
{
}
Expand Down Expand Up @@ -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<TestRuleSetModel>, 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;
}
}
}