Skip to content

Commit cb83c00

Browse files
committed
Added support for calling TryValidateModel
Fixes #53
1 parent dc4a60b commit cb83c00

File tree

6 files changed

+149
-22
lines changed

6 files changed

+149
-22
lines changed

FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,12 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService
3232
serviceCollection.Configure(autoValidationMvcConfiguration);
3333
}
3434

35-
if (configuration.DisableBuiltInModelValidation)
36-
{
37-
serviceCollection.AddSingleton<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceProvider =>
38-
new FluentValidationAutoValidationObjectModelValidator(
39-
serviceProvider.GetRequiredService<IModelMetadataProvider>(),
40-
serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value.ModelValidatorProviders,
41-
configuration.DisableBuiltInModelValidation));
42-
}
35+
serviceCollection.AddSingleton<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceProvider =>
36+
new FluentValidationAutoValidationObjectModelValidator(
37+
serviceProvider,
38+
serviceProvider.GetRequiredService<IModelMetadataProvider>(),
39+
serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value.ModelValidatorProviders,
40+
configuration.DisableBuiltInModelValidation));
4341

4442
// Add the default result factory.
4543
serviceCollection.AddScoped<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>();
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using Microsoft.AspNetCore.Mvc;
34
using Microsoft.AspNetCore.Mvc.ModelBinding;
45
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -7,11 +8,16 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation
78
{
89
public class FluentValidationAutoValidationObjectModelValidator : ObjectModelValidator
910
{
11+
private readonly IServiceProvider serviceProvider;
1012
private readonly bool disableBuiltInModelValidation;
1113

12-
public FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList<IModelValidatorProvider> validatorProviders, bool disableBuiltInModelValidation)
14+
public FluentValidationAutoValidationObjectModelValidator(
15+
IServiceProvider serviceProvider,
16+
IModelMetadataProvider modelMetadataProvider,
17+
IList<IModelValidatorProvider> validatorProviders, bool disableBuiltInModelValidation)
1318
: base(modelMetadataProvider, validatorProviders)
1419
{
20+
this.serviceProvider = serviceProvider;
1521
this.disableBuiltInModelValidation = disableBuiltInModelValidation;
1622
}
1723

@@ -21,7 +27,14 @@ public override ValidationVisitor GetValidationVisitor(ActionContext actionConte
2127
IModelMetadataProvider metadataProvider,
2228
ValidationStateDictionary? validationState)
2329
{
24-
return new FluentValidationAutoValidationValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState, disableBuiltInModelValidation);
30+
return new FluentValidationAutoValidationValidationVisitor(
31+
serviceProvider,
32+
actionContext,
33+
validatorProvider,
34+
validatorCache,
35+
metadataProvider,
36+
validationState,
37+
disableBuiltInModelValidation);
2538
}
2639
}
2740
}
Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,76 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using System;
2+
using System.Linq;
3+
using FluentValidation;
4+
using Microsoft.AspNetCore.Mvc;
25
using Microsoft.AspNetCore.Mvc.ModelBinding;
36
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
47

58
namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation
69
{
710
public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor
811
{
12+
private readonly IServiceProvider serviceProvider;
913
private readonly bool disableBuiltInModelValidation;
1014

11-
public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext,
15+
public FluentValidationAutoValidationValidationVisitor(
16+
IServiceProvider serviceProvider,
17+
ActionContext actionContext,
1218
IModelValidatorProvider validatorProvider,
1319
ValidatorCache validatorCache,
1420
IModelMetadataProvider metadataProvider,
1521
ValidationStateDictionary? validationState,
1622
bool disableBuiltInModelValidation)
1723
: base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState)
1824
{
25+
this.serviceProvider = serviceProvider;
1926
this.disableBuiltInModelValidation = disableBuiltInModelValidation;
2027
}
2128

2229
public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel)
2330
{
2431
// If built in model validation is disabled return true for later validation in the action filter.
25-
return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel);
32+
bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel);
33+
return Validate(isBaseValid, key, model);
2634
}
2735

2836
#if !NETCOREAPP3_1
2937
public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? container)
3038
{
3139
// If built in model validation is disabled return true for later validation in the action filter.
32-
return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container);
40+
bool isBaseValid = disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container);
41+
return Validate(isBaseValid, key, model);
3342
}
3443
#endif
44+
45+
private bool Validate(
46+
bool isBaseValid,
47+
string? key,
48+
object? model)
49+
{
50+
if (model == null)
51+
{
52+
return isBaseValid;
53+
}
54+
55+
// Use FluentValidation to perform additional validation
56+
var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
57+
if (!(this.serviceProvider.GetService(validatorType) is IValidator validator))
58+
{
59+
return isBaseValid;
60+
}
61+
62+
var validationResult = validator.Validate(new ValidationContext<object>(model));
63+
foreach (var error in validationResult.Errors)
64+
{
65+
var keyName = string.IsNullOrEmpty(key) ? error.PropertyName : $"{key}.{error.PropertyName}";
66+
67+
if (!ModelState[keyName]?.Errors.Any(e => e.ErrorMessage == error.ErrorMessage) ?? true)
68+
{
69+
ModelState.AddModelError(keyName, error.ErrorMessage);
70+
}
71+
}
72+
73+
return isBaseValid && validationResult.IsValid;
74+
}
3575
}
3676
}

Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public void TestAddFluentValidationAutoValidation()
2121

2222
AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
2323
AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
24-
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
24+
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
2525
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
2626
}
2727

@@ -34,7 +34,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_OverriddenRe
3434

3535
AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
3636
AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
37-
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
37+
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
3838
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
3939
}
4040

@@ -47,7 +47,7 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuilt
4747

4848
AssertNotContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, TestResultFactory>(serviceCollection, ServiceLifetime.Scoped);
4949
AssertContainsServiceDescriptor<IFluentValidationAutoValidationResultFactory, FluentValidationAutoValidationDefaultResultFactory>(serviceCollection, ServiceLifetime.Scoped);
50-
AssertNotContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
50+
AssertContainsServiceDescriptor<IObjectModelValidator, FluentValidationAutoValidationObjectModelValidator>(serviceCollection, ServiceLifetime.Singleton);
5151
AssertContainsServiceDescriptor<IConfigureOptions<MvcOptions>, ConfigureNamedOptions<MvcOptions>>(serviceCollection, ServiceLifetime.Singleton);
5252
}
5353

Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationObjectModelValidatorTest.cs

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using FluentValidation;
5+
using Microsoft.AspNetCore.Http;
26
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.Controllers;
38
using Microsoft.AspNetCore.Mvc.ModelBinding;
49
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
10+
using Microsoft.AspNetCore.Routing;
11+
using Microsoft.Extensions.DependencyInjection;
512
using NSubstitute;
13+
using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
614
using SharpGrip.FluentValidation.AutoValidation.Mvc.Validation;
715
using Xunit;
816

@@ -13,15 +21,74 @@ public class FluentValidationAutoValidationObjectModelValidatorTest
1321
[Fact]
1422
public void TestGetValidationVisitor()
1523
{
24+
var serviceProvider = Substitute.For<IServiceProvider>();
1625
var modelMetadataProvider = Substitute.For<IModelMetadataProvider>();
1726
var modelMetadataProviders = Substitute.For<IList<IModelValidatorProvider>>();
1827
var actionContext = Substitute.For<ActionContext>();
1928
var modelValidatorProvider = Substitute.For<IModelValidatorProvider>();
2029
var validatorCache = Substitute.For<ValidatorCache>();
2130

22-
var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator(modelMetadataProvider, modelMetadataProviders, true);
31+
var fluentValidationAutoValidationObjectModelValidator = new FluentValidationAutoValidationObjectModelValidator(
32+
serviceProvider, modelMetadataProvider, modelMetadataProviders, true);
2333

2434
Assert.IsType<FluentValidationAutoValidationValidationVisitor>(
2535
fluentValidationAutoValidationObjectModelValidator.GetValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null));
2636
}
37+
38+
[Fact]
39+
public void TryValidateModel_WithInvalidModel_ShouldUpdateModelState()
40+
{
41+
// Arrange
42+
var serviceCollection = new ServiceCollection();
43+
serviceCollection.AddControllersWithViews();
44+
serviceCollection.AddFluentValidationAutoValidation();
45+
serviceCollection.AddTransient<IValidator<Test1Controller.Action1ViewModel>, Test1Controller.Action1ViewModel.Action1ViewModelValidator>();
46+
var serviceProvider = serviceCollection.BuildServiceProvider();
47+
48+
var viewModel = new Test1Controller.Action1ViewModel
49+
{
50+
ValueMustEqual1 = 0 // Invalid value to trigger validation error
51+
};
52+
53+
var httpContext = new DefaultHttpContext
54+
{
55+
RequestServices = serviceProvider
56+
};
57+
var routeData = Substitute.For<RouteData>();
58+
var actionDescriptor = Substitute.For<ControllerActionDescriptor>();
59+
var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);
60+
var controller = new Test1Controller
61+
{
62+
ControllerContext = new ControllerContext(actionContext)
63+
};
64+
65+
// Act
66+
bool result = controller.TryValidateModel(viewModel);
67+
68+
// Assert
69+
Assert.False(result);
70+
Assert.False(controller.ModelState.IsValid);
71+
Assert.True(controller.ModelState.ContainsKey(nameof(Test1Controller.Action1ViewModel.ValueMustEqual1)));
72+
var modelError = controller.ModelState[nameof(Test1Controller.Action1ViewModel.ValueMustEqual1)]!.Errors.Single();
73+
Assert.NotNull(modelError);
74+
Assert.Equal("'ValueMustEqual1' must be equal to '1'.", modelError.ErrorMessage);
75+
}
76+
77+
public class Test1Controller : Controller
78+
{
79+
public class Action1ViewModel
80+
{
81+
public int ValueMustEqual1 { get; set; }
82+
83+
internal class Action1ViewModelValidator : AbstractValidator<Action1ViewModel>
84+
{
85+
public Action1ViewModelValidator()
86+
{
87+
this.RuleFor(x => x.ValueMustEqual1)
88+
.Equal(1)
89+
.WithName(nameof(ValueMustEqual1));
90+
}
91+
}
92+
}
93+
}
2794
}

Tests/src/FluentValidation.AutoValidation.Mvc/Validation/FluentValidationAutoValidationValidationVisitorTest.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using System;
2+
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.AspNetCore.Mvc.ModelBinding;
34
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
45
using NSubstitute;
@@ -12,13 +13,21 @@ public class FluentValidationAutoValidationValidationVisitorTest
1213
[Fact]
1314
public void TestGetValidationVisitor()
1415
{
16+
var serviceProvider = Substitute.For<IServiceProvider>();
1517
var modelMetadataProvider = Substitute.For<IModelMetadataProvider>();
1618
var actionContext = Substitute.For<ActionContext>();
1719
var modelValidatorProvider = Substitute.For<IModelValidatorProvider>();
1820
var validatorCache = Substitute.For<ValidatorCache>();
1921

2022
var fluentValidationAutoValidationObjectModelValidator =
21-
new FluentValidationAutoValidationValidationVisitor(actionContext, modelValidatorProvider, validatorCache, modelMetadataProvider, null, true);
23+
new FluentValidationAutoValidationValidationVisitor(
24+
serviceProvider,
25+
actionContext,
26+
modelValidatorProvider,
27+
validatorCache,
28+
modelMetadataProvider,
29+
null,
30+
true);
2231

2332
#if NETCOREAPP3_1
2433
Assert.True(fluentValidationAutoValidationObjectModelValidator.Validate(null, null, new TestModel(), true));

0 commit comments

Comments
 (0)