Skip to content

Commit eb7fd03

Browse files
authored
Merge pull request #47 from icnocop/master
Use the method parameter's runtime (instance) type to find the validator instead of its declared type
2 parents 0d3d98d + bc5c029 commit eb7fd03

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC
5555
if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject))
5656
{
5757
var parameterInfo = (parameter as ControllerParameterDescriptor)?.ParameterInfo;
58-
var parameterType = parameter.ParameterType;
58+
var parameterType = subject?.GetType();
5959
var bindingSource = parameter.BindingInfo?.BindingSource;
6060

6161
var hasAutoValidateAlwaysAttribute = parameterInfo?.HasCustomAttribute<AutoValidateAlwaysAttribute>() ?? false;
6262
var hasAutoValidateNeverAttribute = parameterInfo?.HasCustomAttribute<AutoValidateNeverAttribute>() ?? false;
6363

64-
if (subject != null && parameterType.IsCustomType() &&
64+
if (subject != null && parameterType != null && parameterType.IsCustomType() &&
6565
!hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) &&
6666
serviceProvider.GetValidator(parameterType) is IValidator validator)
6767
{

Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,111 @@ public async Task TestOnActionExecutionAsync()
101101
Assert.Contains(validationFailuresValues[2].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(TestModel.Parameter3)][0]);
102102
}
103103

104+
[Fact]
105+
public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterType_UsesInstanceTypeValidator()
106+
{
107+
// Arrange
108+
var httpContext = Substitute.For<HttpContext>();
109+
var modelStateDictionary = new ModelStateDictionary();
110+
111+
var validationFailures = new Dictionary<string, string[]>
112+
{
113+
{nameof(CreatePersonRequest.Name), [$"'{nameof(CreatePersonRequest.Name)}' must be equal to 'John Doe'."]}
114+
};
115+
var validationProblemDetails = new ValidationProblemDetails(validationFailures);
116+
117+
var problemDetailsFactory = Substitute.For<ProblemDetailsFactory>();
118+
problemDetailsFactory.CreateValidationProblemDetails(httpContext, modelStateDictionary).Returns(validationProblemDetails);
119+
120+
var serviceProvider = Substitute.For<IServiceProvider>();
121+
serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(CreateAnimalRequest))).Returns(new CreateAnimalRequestValidator());
122+
serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(CreatePersonRequest))).Returns(new CreatePersonRequestValidator());
123+
serviceProvider.GetService(typeof(ProblemDetailsFactory)).Returns(problemDetailsFactory);
124+
125+
httpContext.RequestServices.Returns(serviceProvider);
126+
127+
var controller = Substitute.For<AnimalsController>();
128+
var controllerActionDescriptor = new ControllerActionDescriptor
129+
{
130+
Parameters =
131+
[
132+
new()
133+
{
134+
Name = "request",
135+
ParameterType = typeof(CreateAnimalRequest),
136+
BindingInfo = new BindingInfo {BindingSource = BindingSource.Body}
137+
}
138+
]
139+
};
140+
var actionContext = Substitute.For<ActionContext>(httpContext, Substitute.For<RouteData>(), controllerActionDescriptor, modelStateDictionary);
141+
142+
var actionArguments = new Dictionary<string, object?>
143+
{
144+
{
145+
"request", new CreatePersonRequest
146+
{
147+
Name = "Jane Doe"
148+
}
149+
},
150+
};
151+
var actionExecutingContext = Substitute.For<ActionExecutingContext>(actionContext, new List<IFilterMetadata>(), actionArguments, new object());
152+
actionExecutingContext.Controller.Returns(controller);
153+
actionExecutingContext.ActionDescriptor = controllerActionDescriptor;
154+
actionExecutingContext.ActionArguments.Returns(actionArguments);
155+
156+
var actionExecutedContext = Substitute.For<ActionExecutedContext>(actionContext, new List<IFilterMetadata>(), new object());
157+
158+
var fluentValidationAutoValidationResultFactory = Substitute.For<IFluentValidationAutoValidationResultFactory>();
159+
fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails));
160+
161+
var autoValidationMvcConfiguration = Substitute.For<IOptions<AutoValidationMvcConfiguration>>();
162+
autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration());
163+
164+
var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration);
165+
166+
// Act
167+
await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext));
168+
169+
// Assert
170+
var modelStateDictionaryValues = modelStateDictionary.Values.ToList();
171+
var validationFailuresValues = validationFailures.Values.ToList();
172+
var badRequestObjectResult = (BadRequestObjectResult)actionExecutingContext.Result!;
173+
var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails)badRequestObjectResult.Value!;
174+
175+
Assert.Contains(validationFailuresValues[0].First(), modelStateDictionaryValues[0].Errors.Select(error => error.ErrorMessage));
176+
Assert.Contains(validationFailuresValues[0].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(CreatePersonRequest.Name)][0]);
177+
}
178+
179+
public class AnimalsController : ControllerBase
180+
{
181+
}
182+
183+
public class CreateAnimalRequest
184+
{
185+
}
186+
187+
public class CreatePersonRequest : CreateAnimalRequest
188+
{
189+
public required string Name { get; set; }
190+
}
191+
192+
public class CreateAnimalRequestValidator : AbstractValidator<CreateAnimalRequest>
193+
{
194+
public CreateAnimalRequestValidator()
195+
{
196+
}
197+
}
198+
199+
public class CreatePersonRequestValidator : AbstractValidator<CreatePersonRequest>
200+
{
201+
public CreatePersonRequestValidator()
202+
{
203+
this.Include(new CreateAnimalRequestValidator());
204+
205+
this.RuleFor(x => x.Name).Equal("John Doe");
206+
}
207+
}
208+
104209
public class TestController : ControllerBase
105210
{
106211
}

0 commit comments

Comments
 (0)