From 5545304e6f31bd8e139a5c3e0da67ca050260c47 Mon Sep 17 00:00:00 2001 From: pgauledev <128549236+pgauledev@users.noreply.github.com> Date: Wed, 29 Mar 2023 08:52:43 -0500 Subject: [PATCH] Add optional filter to AddFluentValidationAutoValidation() (#20) --- .../FluentValidationModelValidatorProvider.cs | 34 +- .../FluentValidationMvcConfiguration.cs | 9 + .../FluentValidationMvcExtensions.cs | 3 +- .../Controllers/TestController.cs | 16 + ...ionModelValidatorFilterIntegrationTests.cs | 369 ++++++++++++++++++ .../TestModels.cs | 62 +++ 6 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 src/FluentValidation.Tests.AspNetCore/FluentValidationModelValidatorFilterIntegrationTests.cs diff --git a/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs b/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs index 0d7293d..7cb53dc 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationModelValidatorProvider.cs @@ -37,22 +37,31 @@ namespace FluentValidation.AspNetCore; public class FluentValidationModelValidatorProvider : IModelValidatorProvider { private readonly bool _implicitValidationEnabled; private readonly bool _implicitRootCollectionElementValidationEnabled; + private readonly Func _filter; public FluentValidationModelValidatorProvider(bool implicitValidationEnabled) - : this(implicitValidationEnabled, false) { + : this(implicitValidationEnabled, false, default) { } public FluentValidationModelValidatorProvider( bool implicitValidationEnabled, - bool implicitRootCollectionElementValidationEnabled) { + bool implicitRootCollectionElementValidationEnabled) + : this(implicitValidationEnabled, implicitRootCollectionElementValidationEnabled, default) { + } + + public FluentValidationModelValidatorProvider( + bool implicitValidationEnabled, + bool implicitRootCollectionElementValidationEnabled, + Func filter) { _implicitValidationEnabled = implicitValidationEnabled; _implicitRootCollectionElementValidationEnabled = implicitRootCollectionElementValidationEnabled; + _filter = filter; } public virtual void CreateValidators(ModelValidatorProviderContext context) { context.Results.Add(new ValidatorItem { IsReusable = false, - Validator = new FluentValidationModelValidator(_implicitValidationEnabled, _implicitRootCollectionElementValidationEnabled), + Validator = new FluentValidationModelValidator(_implicitValidationEnabled, _implicitRootCollectionElementValidationEnabled, _filter), }); } } @@ -63,16 +72,25 @@ public virtual void CreateValidators(ModelValidatorProviderContext context) { public class FluentValidationModelValidator : IModelValidator { private readonly bool _implicitValidationEnabled; private readonly bool _implicitRootCollectionElementValidationEnabled; + private readonly Func _filter; public FluentValidationModelValidator(bool implicitValidationEnabled) - : this(implicitValidationEnabled, false) { + : this(implicitValidationEnabled, false, default) { } public FluentValidationModelValidator( bool implicitValidationEnabled, - bool implicitRootCollectionElementValidationEnabled) { + bool implicitRootCollectionElementValidationEnabled) + : this(implicitValidationEnabled, implicitRootCollectionElementValidationEnabled, default) { + } + + public FluentValidationModelValidator( + bool implicitValidationEnabled, + bool implicitRootCollectionElementValidationEnabled, + Func filter) { _implicitValidationEnabled = implicitValidationEnabled; _implicitRootCollectionElementValidationEnabled = implicitRootCollectionElementValidationEnabled; + _filter = filter; } public virtual IEnumerable Validate(ModelValidationContext mvContext) { @@ -140,6 +158,12 @@ public virtual IEnumerable Validate(ModelValidationContex } protected bool ShouldSkip(ModelValidationContext mvContext) { + //Apply custom filter (if specified) + //validation will be skipped unless we match on this filter + if (_filter != null && !_filter.Invoke(mvContext.ModelMetadata.ModelType)) { + return true; + } + // Skip if there's nothing to process. if (mvContext.Model == null) { return true; diff --git a/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs b/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs index ebafa5f..f2c5377 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationMvcConfiguration.cs @@ -60,6 +60,15 @@ public class FluentValidationAutoValidationConfiguration { /// Setting this to true will disable DataAnnotations and only run FluentValidation. /// public bool DisableDataAnnotationsValidation { get; set; } + + + /// + /// When specified, automatic validation will only apply to the types matched by the filter. + /// If the filter does not match, automatic validation will not be applied. This can be useful + /// for specific situations where you want to opt in/out of automatic validation + /// Example: Filter = type => type == typeof(Model) + /// + public Func Filter { get; set; } } /// diff --git a/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs b/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs index 5bbaaa7..f7c5ebb 100644 --- a/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs +++ b/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs @@ -139,7 +139,8 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService if (!options.ModelValidatorProviders.Any(x => x is FluentValidationModelValidatorProvider)) { options.ModelValidatorProviders.Insert(0, new FluentValidationModelValidatorProvider( config.ImplicitlyValidateChildProperties, - config.ImplicitlyValidateRootCollectionElements)); + config.ImplicitlyValidateRootCollectionElements, + config.Filter)); } }); diff --git a/src/FluentValidation.Tests.AspNetCore/Controllers/TestController.cs b/src/FluentValidation.Tests.AspNetCore/Controllers/TestController.cs index f8dfbd6..18db949 100644 --- a/src/FluentValidation.Tests.AspNetCore/Controllers/TestController.cs +++ b/src/FluentValidation.Tests.AspNetCore/Controllers/TestController.cs @@ -206,6 +206,22 @@ public async Task UpdateModel() { return TestResult(); } + public ActionResult AutoFilter(AutoFilterModel test) { + return TestResult(); + } + + public ActionResult AutoFilterParent(AutoFilterParentModel test) { + return TestResult(); + } + + public ActionResult AutoFilterRootCollection(List test) { + return TestResult(); + } + + public ActionResult AutoFilterParentWithCollection(AutoFilterParentWithCollectionModel test) { + return TestResult(); + } + private ActionResult TestResult() { var errors = new List(); diff --git a/src/FluentValidation.Tests.AspNetCore/FluentValidationModelValidatorFilterIntegrationTests.cs b/src/FluentValidation.Tests.AspNetCore/FluentValidationModelValidatorFilterIntegrationTests.cs new file mode 100644 index 0000000..bf826ec --- /dev/null +++ b/src/FluentValidation.Tests.AspNetCore/FluentValidationModelValidatorFilterIntegrationTests.cs @@ -0,0 +1,369 @@ +namespace FluentValidation.Tests; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Controllers; +using FluentValidation.AspNetCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; +using FormData = System.Collections.Generic.Dictionary; + +public class FluentValidationModelValidatorFilterIntegrationTests : IClassFixture { + private readonly WebAppFixture _webApp; + + public FluentValidationModelValidatorFilterIntegrationTests(ITestOutputHelper output, WebAppFixture webApp) { + CultureScope.SetDefaultCulture(); + _webApp = webApp; + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Not_Run_Validation_If_Filter_Does_Not_Match_ModelType() { + var form = new FormData { + {"Email", "foo"}, + {"Surname", "foo"}, + {"Forename", "foo"}, + {"DateOfBirth", null}, + {"Address1", null} + }; + + // model type opted out - do not run auto validation + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType != typeof(AutoFilterModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilter", form); + + result.Count.ShouldEqual(0); + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_If_No_Filter_Specified() { + var form = new FormData { + {"Email", "foo"}, + {"Surname", "foo"}, + {"Forename", "foo"}, + {"DateOfBirth", null}, + {"Address1", null} + }; + + // No filter specified - we run validation rules as normal + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilter", form); + + result.Count.ShouldEqual(4); + result.IsValidField("Email").ShouldBeFalse(); //Email validation failed + result.IsValidField("DateOfBirth").ShouldBeFalse(); //Date of Birth not specified + result.IsValidField("Surname").ShouldBeFalse(); //Surname not specified + result.IsValidField("Address1").ShouldBeFalse(); //Address1 not specified + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_If_Filter_Matches_ModelType() { + var form = new FormData { + {"Email", "foo"}, + {"Surname", "foo"}, + {"Forename", "foo"}, + {"DateOfBirth", null}, + {"Address1", null} + }; + + //AutoFilterChildModel opted into auto validation - will run + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(AutoFilterModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilter", form); + + result.Count.ShouldEqual(4); + result.IsValidField("Email").ShouldBeFalse(); //Email validation failed + result.IsValidField("DateOfBirth").ShouldBeFalse(); //Date of Birth not specified + result.IsValidField("Surname").ShouldBeFalse(); //Surname not specified + result.IsValidField("Address1").ShouldBeFalse(); //Address1 not specified + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Not_Run_Validation_If_Filter_Only_Includes_Child_Type() { + var form = new FormData { + {"Id", null}, + {"ChildModel", null} + }; + + // Child model is "opted in", but parent is not - we skip validation entirely because auto validation looks at top-level type + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(AutoFilterModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterParent", form); + + result.Count.ShouldEqual(0); + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_If_Filter_Returns_True_For_Parent() { + var form = new FormData { + {"Id", null}, + {"ChildModel.Email", "foo"}, + {"ChildModel.Surname", "foo"}, + {"ChildModel.Forename", "foo"}, + {"ChildModel.DateOfBirth", null}, + {"ChildModel.Address1", null} + }; + + // Parent model is "opted in" - we should run all validation rules + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(AutoFilterParentModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterParent", form); + + result.Count.ShouldEqual(5); + result.IsValidField("Id").ShouldBeFalse(); //Id not specified for parent + result.IsValidField("ChildModel.Email").ShouldBeFalse(); //Email validation failed for child + result.IsValidField("ChildModel.DateOfBirth").ShouldBeFalse(); //Date of Birth not specified for child + result.IsValidField("ChildModel.Surname").ShouldBeFalse(); //surname not specified for child + result.IsValidField("ChildModel.Address1").ShouldBeFalse(); //Address1 not specified for child + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Not_Run_Validation_For_Root_Collection_If_Filter_Does_Not_Match_CollectionType() { + var form = new FormData { + {"test[0].Email", "foo"}, + {"test[0].Surname", "foo"}, + {"test[0].Forename", "foo"}, + {"test[0].DateOfBirth", null}, + {"test[0].Address1", null} + }; + + // collection type is not opted in - should not run validation + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(AutoFilterModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterRootCollection", form); + + result.Count.ShouldEqual(0); + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_For_Root_Collection_If_Filter_Matches_CollectionType() { + var form = new FormData { + {"test[0].Email", "foo"}, + {"test[0].Surname", "foo"}, + {"test[0].Forename", "foo"}, + {"test[0].DateOfBirth", null}, + {"test[0].Address1", null}, + {"test[1].Email", "foo"}, + {"test[1].Surname", "foobar"}, + {"test[1].Forename", "foo"}, + {"test[1].DateOfBirth", DateTime.UtcNow.Date.ToString()}, + {"test[1].Address1", "foo"} + }; + + // collection type is opted in - should run validation + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(List); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterRootCollection", form); + + result.Count.ShouldEqual(5); + result.IsValidField("test.x[0].Email").ShouldBeFalse(); //Email validation failed for collection item #1 + result.IsValidField("test.x[0].DateOfBirth").ShouldBeFalse(); //Date of Birth not specified for collection item #1 + result.IsValidField("test.x[0].Surname").ShouldBeFalse(); //surname not specified for collection item #1 + result.IsValidField("test.x[0].Address1").ShouldBeFalse(); //Address1 not specified for collection item #1 + result.IsValidField("test.x[1].Email").ShouldBeFalse(); //Email validation failed for collection item #2 + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_For_Root_Collection_If_No_Filter_Specified() { + var form = new FormData { + {"test[0].Email", "foo"}, + {"test[0].Surname", "foo"}, + {"test[0].Forename", "foo"}, + {"test[0].DateOfBirth", null}, + {"test[0].Address1", null}, + {"test[1].Email", "foo"}, + {"test[1].Surname", "foobar"}, + {"test[1].Forename", "foo"}, + {"test[1].DateOfBirth", DateTime.UtcNow.Date.ToString()}, + {"test[1].Address1", "foo"} + }; + + // no filter applied - run collection validation as normal + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterRootCollection", form); + + result.Count.ShouldEqual(5); + result.IsValidField("test.x[0].Email").ShouldBeFalse(); //Email validation failed for collection item #1 + result.IsValidField("test.x[0].DateOfBirth").ShouldBeFalse(); //Date of Birth not specified for collection item #1 + result.IsValidField("test.x[0].Surname").ShouldBeFalse(); //surname not specified for collection item #1 + result.IsValidField("test.x[0].Address1").ShouldBeFalse(); //Address1 not specified for collection item #1 + result.IsValidField("test.x[1].Email").ShouldBeFalse(); //Email validation failed for collection item #2 + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Not_Run_Validation_For_Parent_With_Collection_If_Filter_Does_Not_Match_Type() { + var form = new FormData { + {"test.Id", null}, + {"test.ChildModels[0].Surname", "foo"}, + {"test.ChildModels[0].Forename", "foo"}, + {"test.ChildModels[0].DateOfBirth", null}, + {"test.ChildModels[0].Address1", null}, + {"test.ChildModels[1].Email", "foo"}, + {"test.ChildModels[1].Surname", "foobar"}, + {"test.ChildModels[1].Forename", "foo"}, + {"test.ChildModels[1].DateOfBirth", DateTime.UtcNow.Date.ToString()}, + {"test.ChildModels[1].Address1", "foo"} + }; + + // opt out of automatic validation - don't run validation + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType != typeof(AutoFilterParentWithCollectionModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterParentWithCollection", form); + + result.Count.ShouldEqual(0); + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_For_Parent_With_Collection_If_Filter_Matches_Type() { + var form = new FormData { + {"test.Id", null}, + {"test.ChildModels[0].Surname", "foo"}, + {"test.ChildModels[0].Forename", "foo"}, + {"test.ChildModels[0].DateOfBirth", null}, + {"test.ChildModels[0].Address1", null}, + {"test.ChildModels[1].Email", "foo"}, + {"test.ChildModels[1].Surname", "foobar"}, + {"test.ChildModels[1].Forename", "foo"}, + {"test.ChildModels[1].DateOfBirth", DateTime.UtcNow.Date.ToString()}, + {"test.ChildModels[1].Address1", "foo"} + }; + + // opt into auto validation -> run validation + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(cfg => { + cfg.Filter = modelType => modelType == typeof(AutoFilterParentWithCollectionModel); + }); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterParentWithCollection", form); + + result.Count.ShouldEqual(6); + result.IsValidField("test.Id").ShouldBeFalse(); //Is not specified at root + result.IsValidField("test.ChildModels[0].Email").ShouldBeFalse(); //Email validation failed for collection item #1 + result.IsValidField("test.ChildModels[0].DateOfBirth").ShouldBeFalse(); //Date of Birth not specified for collection item #1 + result.IsValidField("test.ChildModels[0].Surname").ShouldBeFalse(); //surname not specified for collection item #1 + result.IsValidField("test.ChildModels[0].Address1").ShouldBeFalse(); //Address1 not specified for collection item #1 + result.IsValidField("test.ChildModels[1].Email").ShouldBeFalse(); //Email validation failed for collection item #2 + } + + [Fact] + public async Task AddFluentValidationAutoValidation_Should_Run_Validation_For_Parent_With_Collection_If_No_Filter_Specified() { + var form = new FormData { + {"test.Id", null}, + {"test.ChildModels[0].Surname", "foo"}, + {"test.ChildModels[0].Forename", "foo"}, + {"test.ChildModels[0].DateOfBirth", null}, + {"test.ChildModels[0].Address1", null}, + {"test.ChildModels[1].Email", "foo"}, + {"test.ChildModels[1].Surname", "foobar"}, + {"test.ChildModels[1].Forename", "foo"}, + {"test.ChildModels[1].DateOfBirth", DateTime.UtcNow.Date.ToString()}, + {"test.ChildModels[1].Address1", "foo"} + }; + + // no filter applied - run validation as normal + var client = _webApp.CreateClientWithServices(services => { + services.AddFluentValidationAutoValidation(); + services.AddMvc().AddNewtonsoftJson(); + services.AddScoped, AutoFilterParentWithCollectionModelValidator>(); + services.AddScoped>, AutoFilterRootCollectionValidator>(); + services.AddScoped, AutoFilterParentModelValidator>(); + services.AddScoped, AutoFilterChildModelValidator>(); + }); + + var result = await client.GetErrors("AutoFilterParentWithCollection", form); + + result.Count.ShouldEqual(6); + result.IsValidField("test.Id").ShouldBeFalse(); //Is not specified at root + result.IsValidField("test.ChildModels[0].Email").ShouldBeFalse(); //Email validation failed for collection item #1 + result.IsValidField("test.ChildModels[0].DateOfBirth").ShouldBeFalse(); //Date of Birth not specified for collection item #1 + result.IsValidField("test.ChildModels[0].Surname").ShouldBeFalse(); //surname not specified for collection item #1 + result.IsValidField("test.ChildModels[0].Address1").ShouldBeFalse(); //Address1 not specified for collection item #1 + result.IsValidField("test.ChildModels[1].Email").ShouldBeFalse(); //Email validation failed for collection item #2 + } +} diff --git a/src/FluentValidation.Tests.AspNetCore/TestModels.cs b/src/FluentValidation.Tests.AspNetCore/TestModels.cs index 484f79f..d76b6a6 100644 --- a/src/FluentValidation.Tests.AspNetCore/TestModels.cs +++ b/src/FluentValidation.Tests.AspNetCore/TestModels.cs @@ -477,3 +477,65 @@ public ChildRecordValidator() { } } #endif + +public class AutoFilterParentWithCollectionModel{ + public int? Id { get; set; } + public List ChildModels { get; set;} +} + +public class AutoFilterParentModel { + public int? Id { get; set; } + public AutoFilterModel ChildModel { get; set;} +} + +public class AutoFilterModel { + public string Surname { get; set; } + public string Forename { get; set; } + public string Email { get; set; } + public DateTime? DateOfBirth { get; set; } + public string Address1 { get; set; } +} + +public class AutoFilterChildModelValidator : AbstractValidator { + public AutoFilterChildModelValidator() { + RuleFor(x => x.Surname) + .NotEmpty() + .NotEqual(x => x.Forename); + + RuleFor(x => x.Email) + .NotEmpty() + .EmailAddress(); + + RuleFor(x => x.DateOfBirth) + .NotEmpty(); + + RuleFor(x => x.Address1).NotEmpty(); + } +} + +public class AutoFilterParentModelValidator : AbstractValidator { + public AutoFilterParentModelValidator() { + RuleFor(x => x.Id) + .NotEmpty(); + + RuleFor(x => x.ChildModel) + .NotEmpty() + .SetValidator(new AutoFilterChildModelValidator()); + } +} + +public class AutoFilterRootCollectionValidator: AbstractValidator> { + public AutoFilterRootCollectionValidator() { + RuleForEach(x => x).SetValidator(new AutoFilterChildModelValidator()); + } +} + +public class AutoFilterParentWithCollectionModelValidator : AbstractValidator { + public AutoFilterParentWithCollectionModelValidator() { + RuleFor(x => x.Id) + .NotEmpty(); + + RuleForEach(x => x.ChildModels).SetValidator(new AutoFilterChildModelValidator()); + } +} +