Skip to content

Commit 2d33c32

Browse files
bordecalpranavkm
authored andcommitted
Allow ValidationVisitor.ValidateComplexTypesIfChildValidationFails to be configured via MvcOptions (dotnet#9312)
* Allow ValidationVisitor.ValidateComplexTypesIfChildValidationFails to be configured via MvcOptions (dotnet#8519) * Regenerated reference source for Mvc.Core to add MvcOptions.ValidateComplexTypesIfChildValidationFails * Simplified functional tests for MvcOptions.ValidateComplexTypesIfChildValidationFails usage scenarios
1 parent 77424a6 commit 2d33c32

File tree

10 files changed

+253
-43
lines changed

10 files changed

+253
-43
lines changed

src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ public MvcOptions() { }
886886
public bool SuppressAsyncSuffixInActionNames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
887887
public bool SuppressInputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
888888
public bool SuppressOutputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
889+
public bool ValidateComplexTypesIfChildValidationFails { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
889890
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.ModelBinding.IValueProviderFactory> ValueProviderFactories { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
890891
System.Collections.Generic.IEnumerator<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch>.GetEnumerator() { throw null; }
891892
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }

src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultObjectValidator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public override ValidationVisitor GetValidationVisitor(
4242
validationState)
4343
{
4444
MaxValidationDepth = _mvcOptions.MaxValidationDepth,
45+
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
4546
};
4647

4748
return visitor;

src/Mvc/Mvc.Core/src/MvcOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,16 @@ public int? MaxValidationDepth
227227
}
228228
}
229229

230+
/// <summary>
231+
/// Gets or sets a value that determines whether the validation visitor will perform validation of a complex type
232+
/// if validation fails for any of its children.
233+
/// <seealso cref="ValidationVisitor.ValidateComplexTypesIfChildValidationFails"/>
234+
/// </summary>
235+
/// <value>
236+
/// The default value is <see langword="false"/>.
237+
/// </value>
238+
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
239+
230240
/// <summary>
231241
/// Gets or sets a value that determines if MVC will remove the suffix "Async" applied to
232242
/// controller action names.

src/Mvc/Mvc.Core/test/ModelBinding/Validation/DefaultObjectValidatorTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,33 @@ public void Validate_Throws_WithMaxDepth_1()
13281328
Assert.NotNull(ex.HelpLink);
13291329
}
13301330

1331+
[Theory]
1332+
[InlineData(false, ModelValidationState.Unvalidated)]
1333+
[InlineData(true, ModelValidationState.Invalid)]
1334+
public void Validate_RespectsMvcOptionsConfiguration_WhenChildValidationFails(bool optionValue, ModelValidationState expectedParentValidationState)
1335+
{
1336+
// Arrange
1337+
_options.ValidateComplexTypesIfChildValidationFails = optionValue;
1338+
1339+
var actionContext = new ActionContext();
1340+
var validationState = new ValidationStateDictionary();
1341+
var validator = CreateValidator();
1342+
1343+
var model = (object)new SelfValidatableModelContainer
1344+
{
1345+
IsParentValid = false,
1346+
ValidatableModelProperty = new ValidatableModel()
1347+
};
1348+
1349+
// Act
1350+
validator.Validate(actionContext, validationState, prefix: string.Empty, model);
1351+
1352+
// Assert
1353+
var modelState = actionContext.ModelState;
1354+
Assert.False(modelState.IsValid);
1355+
Assert.Equal(expectedParentValidationState, modelState.Root.ValidationState);
1356+
}
1357+
13311358
[Fact]
13321359
public void Validate_TypeWithoutValidators()
13331360
{
@@ -1522,6 +1549,22 @@ private class ValidatableModelContainer
15221549
public ValidatableModel ValidatableModelProperty { get; set; }
15231550
}
15241551

1552+
private class SelfValidatableModelContainer : IValidatableObject
1553+
{
1554+
public bool IsParentValid { get; set; } = true;
1555+
1556+
[Display(Name = "Never valid")]
1557+
public ValidatableModel ValidatableModelProperty { get; set; }
1558+
1559+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
1560+
{
1561+
if (!IsParentValid)
1562+
{
1563+
yield return new ValidationResult("Parent not valid");
1564+
}
1565+
}
1566+
}
1567+
15251568
private class TypeThatOverridesEquals
15261569
{
15271570
[StringLength(2)]

src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Text;
99
using System.Threading.Tasks;
1010
using FormatterWebSite;
11+
using FormatterWebSite.Models;
1112
using Microsoft.AspNetCore.Http;
1213
using Microsoft.AspNetCore.Testing.xunit;
1314
using Newtonsoft.Json;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Http;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using FormatterWebSite.Models;
11+
using Microsoft.AspNetCore.Hosting;
12+
using Microsoft.AspNetCore.Http;
13+
using Newtonsoft.Json;
14+
using Xunit;
15+
16+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
17+
{
18+
/// <summary>
19+
/// Functional tests for verifying the impact of using <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
20+
/// </summary>
21+
public class InputParentValidationTests
22+
{
23+
public abstract class BaseTests<TStartup> : IClassFixture<MvcTestFixture<TStartup>>
24+
where TStartup : class
25+
{
26+
protected BaseTests(MvcTestFixture<TStartup> fixture)
27+
{
28+
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder =>
29+
builder.UseStartup<TStartup>());
30+
31+
Client = factory.CreateDefaultClient();
32+
}
33+
34+
protected abstract bool ShouldParentBeValidatedWhenChildIsInvalid { get; }
35+
36+
private HttpClient Client { get; }
37+
38+
[Fact]
39+
public async Task ParentObjectValidation_RespectsMvcOptions_WhenChildIsInvalid()
40+
{
41+
// Arrange
42+
var content = CreateInvalidModel(false);
43+
var expectedErrors = this.GetExpectedErrors(this.ShouldParentBeValidatedWhenChildIsInvalid, true);
44+
45+
// Act
46+
var response = await Client.PostAsync("http://localhost/Validation/CreateInvalidModel", content);
47+
48+
// Assert
49+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
50+
51+
var responseContent = await response.Content.ReadAsStringAsync();
52+
var actualErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
53+
54+
Assert.Equal(expectedErrors, actualErrors);
55+
}
56+
57+
[Fact]
58+
public async Task ParentObjectIsValidated_WhenChildIsValid()
59+
{
60+
// Arrange
61+
var content = CreateInvalidModel(true);
62+
var expectedErrors = this.GetExpectedErrors(true, false);
63+
64+
// Act
65+
var response = await Client.PostAsync("http://localhost/Validation/CreateInvalidModel", content);
66+
67+
// Assert
68+
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
69+
70+
var responseContent = await response.Content.ReadAsStringAsync();
71+
var actualErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
72+
73+
Assert.Equal(expectedErrors, actualErrors);
74+
}
75+
76+
private StringContent CreateInvalidModel(bool isChildValid)
77+
{
78+
var model = new InvalidModel()
79+
{
80+
Name = (isChildValid ? "Valid Name" : null)
81+
};
82+
83+
return new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
84+
}
85+
86+
private IDictionary<string, string[]> GetExpectedErrors(bool parentInvalid, bool childInvalid)
87+
{
88+
var result = new Dictionary<string, string[]>();
89+
90+
if (parentInvalid)
91+
{
92+
result.Add(string.Empty, new string[] { "The model is not valid." });
93+
}
94+
95+
if (childInvalid)
96+
{
97+
result.Add("Name", new string[] { "The Name field is required." });
98+
}
99+
100+
return result;
101+
}
102+
}
103+
104+
/// <summary>
105+
/// Scenarios for verifying the impact of setting <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
106+
/// to <see langword="true"/>
107+
/// </summary>
108+
public class ParentValidationScenarios : BaseTests<FormatterWebSite.StartupWithComplexParentValidation>
109+
{
110+
public ParentValidationScenarios(MvcTestFixture<FormatterWebSite.StartupWithComplexParentValidation> fixture)
111+
: base(fixture)
112+
{
113+
}
114+
115+
protected override bool ShouldParentBeValidatedWhenChildIsInvalid => true;
116+
}
117+
118+
/// <summary>
119+
/// Scenarios for verifying the impact of leaving <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
120+
/// to its default <see langword="false"/> value
121+
/// </summary>
122+
public class ParentNonValidationScenarios : BaseTests<FormatterWebSite.Startup>
123+
{
124+
public ParentNonValidationScenarios(MvcTestFixture<FormatterWebSite.Startup> fixture)
125+
: base(fixture)
126+
{
127+
}
128+
129+
protected override bool ShouldParentBeValidatedWhenChildIsInvalid => false;
130+
}
131+
}
132+
}

src/Mvc/test/Mvc.IntegrationTests/TryValidateModelIntegrationTest.cs

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.ComponentModel.DataAnnotations;
77
using System.Linq;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
9-
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
109
using Microsoft.AspNetCore.Testing;
1110
using Microsoft.Extensions.DependencyInjection;
1211
using Microsoft.Extensions.Options;
@@ -141,11 +140,7 @@ public void ValidationVisitor_ValidateComplexTypesIfChildValidationFailsSetToTru
141140
var testContext = ModelBindingTestHelper.GetTestContext();
142141
var modelState = testContext.ModelState;
143142
var model = new ModelLevelErrorTest();
144-
var controller = CreateController(testContext, testContext.MetadataProvider);
145-
controller.ObjectValidator = new CustomObjectValidator(testContext.MetadataProvider, TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders)
146-
{
147-
ValidateComplexTypesIfChildValidationFails = true
148-
};
143+
var controller = CreateController(testContext, testContext.MetadataProvider, o => o.ValidateComplexTypesIfChildValidationFails = true);
149144

150145
// Act
151146
var result = controller.TryValidateModel(model);
@@ -166,11 +161,7 @@ public void ValidationVisitor_ValidateComplexTypesIfChildValidationFailsSetToFal
166161
var testContext = ModelBindingTestHelper.GetTestContext();
167162
var modelState = testContext.ModelState;
168163
var model = new ModelLevelErrorTest();
169-
var controller = CreateController(testContext, testContext.MetadataProvider);
170-
controller.ObjectValidator = new CustomObjectValidator(testContext.MetadataProvider, TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders)
171-
{
172-
ValidateComplexTypesIfChildValidationFails= false
173-
};
164+
var controller = CreateController(testContext, testContext.MetadataProvider, o => o.ValidateComplexTypesIfChildValidationFails = false);
174165

175166
// Act
176167
var result = controller.TryValidateModel(model);
@@ -213,8 +204,18 @@ private void AssertErrorEquals(string expected, string actual)
213204
private TestController CreateController(
214205
ActionContext actionContext,
215206
IModelMetadataProvider metadataProvider)
207+
{
208+
return CreateController(actionContext, metadataProvider, _ => { });
209+
}
210+
211+
private TestController CreateController(
212+
ActionContext actionContext,
213+
IModelMetadataProvider metadataProvider,
214+
Action<MvcOptions> optionsConfigurator
215+
)
216216
{
217217
var options = actionContext.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>();
218+
optionsConfigurator.Invoke(options.Value);
218219

219220
var controller = new TestController();
220221
controller.ControllerContext = new ControllerContext(actionContext);
@@ -249,37 +250,5 @@ private Dictionary<string, string> GetModelStateErrors(ModelStateDictionary mode
249250
private class TestController : Controller
250251
{
251252
}
252-
253-
private class CustomObjectValidator : IObjectModelValidator
254-
{
255-
private readonly IModelMetadataProvider _modelMetadataProvider;
256-
private readonly IList<IModelValidatorProvider> _validatorProviders;
257-
private ValidatorCache _validatorCache;
258-
private CompositeModelValidatorProvider _validatorProvider;
259-
260-
public CustomObjectValidator(IModelMetadataProvider modelMetadataProvider, IList<IModelValidatorProvider> validatorProviders)
261-
{
262-
_modelMetadataProvider = modelMetadataProvider;
263-
_validatorProviders = validatorProviders;
264-
_validatorCache = new ValidatorCache();
265-
_validatorProvider = new CompositeModelValidatorProvider(validatorProviders);
266-
}
267-
268-
public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model)
269-
{
270-
var visitor = new ValidationVisitor(
271-
actionContext,
272-
_validatorProvider,
273-
_validatorCache,
274-
_modelMetadataProvider,
275-
validationState);
276-
277-
var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType());
278-
visitor.ValidateComplexTypesIfChildValidationFails = ValidateComplexTypesIfChildValidationFails;
279-
visitor.Validate(metadata, prefix, model);
280-
}
281-
282-
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
283-
}
284253
}
285254
}

src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using FormatterWebSite.Models;
56
using Microsoft.AspNetCore.Mvc;
67

78
namespace FormatterWebSite
@@ -84,5 +85,12 @@ public IActionResult ValidationThrowsError_WhenValidationExceedsMaxValidationDep
8485
{
8586
return Ok();
8687
}
88+
89+
[HttpPost]
90+
[ModelStateValidationFilter]
91+
public IActionResult CreateInvalidModel([FromBody] InvalidModel model)
92+
{
93+
return Ok(model);
94+
}
8795
}
8896
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace FormatterWebSite.Models
5+
{
6+
public class InvalidModel : IValidatableObject
7+
{
8+
[Required]
9+
public string Name { get; set; }
10+
11+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
12+
{
13+
yield return new ValidationResult("The model is not valid.");
14+
}
15+
}
16+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace FormatterWebSite
9+
{
10+
public class StartupWithComplexParentValidation
11+
{
12+
public void ConfigureServices(IServiceCollection services)
13+
{
14+
services
15+
.AddControllers(options => options.ValidateComplexTypesIfChildValidationFails = true)
16+
.AddNewtonsoftJson(options => options.SerializerSettings.Converters.Insert(0, new IModelConverter()))
17+
.SetCompatibilityVersion(CompatibilityVersion.Latest);
18+
}
19+
20+
public void Configure(IApplicationBuilder app)
21+
{
22+
app.UseRouting();
23+
app.UseEndpoints(endpoints =>
24+
{
25+
endpoints.MapDefaultControllerRoute();
26+
});
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)