Allow ValidationVisitor.ValidateComplexTypesIfChildValidationFails to be configured via MvcOptions (#9312)

* Allow ValidationVisitor.ValidateComplexTypesIfChildValidationFails to be configured via MvcOptions (#8519)

* Regenerated reference source for Mvc.Core to add MvcOptions.ValidateComplexTypesIfChildValidationFails

* Simplified functional tests for MvcOptions.ValidateComplexTypesIfChildValidationFails usage scenarios
This commit is contained in:
bordecal 2019-05-05 10:08:25 -04:00 committed by Pranav K
parent 77424a6b06
commit 2d33c32187
10 changed files with 253 additions and 43 deletions

View File

@ -886,6 +886,7 @@ namespace Microsoft.AspNetCore.Mvc
public bool SuppressAsyncSuffixInActionNames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressInputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressOutputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ValidateComplexTypesIfChildValidationFails { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.ModelBinding.IValueProviderFactory> ValueProviderFactories { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
System.Collections.Generic.IEnumerator<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch>.GetEnumerator() { throw null; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }

View File

@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
validationState)
{
MaxValidationDepth = _mvcOptions.MaxValidationDepth,
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
};
return visitor;

View File

@ -227,6 +227,16 @@ namespace Microsoft.AspNetCore.Mvc
}
}
/// <summary>
/// Gets or sets a value that determines whether the validation visitor will perform validation of a complex type
/// if validation fails for any of its children.
/// <seealso cref="ValidationVisitor.ValidateComplexTypesIfChildValidationFails"/>
/// </summary>
/// <value>
/// The default value is <see langword="false"/>.
/// </value>
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
/// <summary>
/// Gets or sets a value that determines if MVC will remove the suffix "Async" applied to
/// controller action names.

View File

@ -1328,6 +1328,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
Assert.NotNull(ex.HelpLink);
}
[Theory]
[InlineData(false, ModelValidationState.Unvalidated)]
[InlineData(true, ModelValidationState.Invalid)]
public void Validate_RespectsMvcOptionsConfiguration_WhenChildValidationFails(bool optionValue, ModelValidationState expectedParentValidationState)
{
// Arrange
_options.ValidateComplexTypesIfChildValidationFails = optionValue;
var actionContext = new ActionContext();
var validationState = new ValidationStateDictionary();
var validator = CreateValidator();
var model = (object)new SelfValidatableModelContainer
{
IsParentValid = false,
ValidatableModelProperty = new ValidatableModel()
};
// Act
validator.Validate(actionContext, validationState, prefix: string.Empty, model);
// Assert
var modelState = actionContext.ModelState;
Assert.False(modelState.IsValid);
Assert.Equal(expectedParentValidationState, modelState.Root.ValidationState);
}
[Fact]
public void Validate_TypeWithoutValidators()
{
@ -1522,6 +1549,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public ValidatableModel ValidatableModelProperty { get; set; }
}
private class SelfValidatableModelContainer : IValidatableObject
{
public bool IsParentValid { get; set; } = true;
[Display(Name = "Never valid")]
public ValidatableModel ValidatableModelProperty { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!IsParentValid)
{
yield return new ValidationResult("Parent not valid");
}
}
}
private class TypeThatOverridesEquals
{
[StringLength(2)]

View File

@ -8,6 +8,7 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using FormatterWebSite;
using FormatterWebSite.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;

View File

@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using FormatterWebSite.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
/// <summary>
/// Functional tests for verifying the impact of using <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
/// </summary>
public class InputParentValidationTests
{
public abstract class BaseTests<TStartup> : IClassFixture<MvcTestFixture<TStartup>>
where TStartup : class
{
protected BaseTests(MvcTestFixture<TStartup> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder =>
builder.UseStartup<TStartup>());
Client = factory.CreateDefaultClient();
}
protected abstract bool ShouldParentBeValidatedWhenChildIsInvalid { get; }
private HttpClient Client { get; }
[Fact]
public async Task ParentObjectValidation_RespectsMvcOptions_WhenChildIsInvalid()
{
// Arrange
var content = CreateInvalidModel(false);
var expectedErrors = this.GetExpectedErrors(this.ShouldParentBeValidatedWhenChildIsInvalid, true);
// Act
var response = await Client.PostAsync("http://localhost/Validation/CreateInvalidModel", content);
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var actualErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
Assert.Equal(expectedErrors, actualErrors);
}
[Fact]
public async Task ParentObjectIsValidated_WhenChildIsValid()
{
// Arrange
var content = CreateInvalidModel(true);
var expectedErrors = this.GetExpectedErrors(true, false);
// Act
var response = await Client.PostAsync("http://localhost/Validation/CreateInvalidModel", content);
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var actualErrors = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
Assert.Equal(expectedErrors, actualErrors);
}
private StringContent CreateInvalidModel(bool isChildValid)
{
var model = new InvalidModel()
{
Name = (isChildValid ? "Valid Name" : null)
};
return new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
}
private IDictionary<string, string[]> GetExpectedErrors(bool parentInvalid, bool childInvalid)
{
var result = new Dictionary<string, string[]>();
if (parentInvalid)
{
result.Add(string.Empty, new string[] { "The model is not valid." });
}
if (childInvalid)
{
result.Add("Name", new string[] { "The Name field is required." });
}
return result;
}
}
/// <summary>
/// Scenarios for verifying the impact of setting <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
/// to <see langword="true"/>
/// </summary>
public class ParentValidationScenarios : BaseTests<FormatterWebSite.StartupWithComplexParentValidation>
{
public ParentValidationScenarios(MvcTestFixture<FormatterWebSite.StartupWithComplexParentValidation> fixture)
: base(fixture)
{
}
protected override bool ShouldParentBeValidatedWhenChildIsInvalid => true;
}
/// <summary>
/// Scenarios for verifying the impact of leaving <see cref="MvcOptions.ValidateComplexTypesIfChildValidationFails"/>
/// to its default <see langword="false"/> value
/// </summary>
public class ParentNonValidationScenarios : BaseTests<FormatterWebSite.Startup>
{
public ParentNonValidationScenarios(MvcTestFixture<FormatterWebSite.Startup> fixture)
: base(fixture)
{
}
protected override bool ShouldParentBeValidatedWhenChildIsInvalid => false;
}
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -141,11 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;
var model = new ModelLevelErrorTest();
var controller = CreateController(testContext, testContext.MetadataProvider);
controller.ObjectValidator = new CustomObjectValidator(testContext.MetadataProvider, TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders)
{
ValidateComplexTypesIfChildValidationFails = true
};
var controller = CreateController(testContext, testContext.MetadataProvider, o => o.ValidateComplexTypesIfChildValidationFails = true);
// Act
var result = controller.TryValidateModel(model);
@ -166,11 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;
var model = new ModelLevelErrorTest();
var controller = CreateController(testContext, testContext.MetadataProvider);
controller.ObjectValidator = new CustomObjectValidator(testContext.MetadataProvider, TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders)
{
ValidateComplexTypesIfChildValidationFails= false
};
var controller = CreateController(testContext, testContext.MetadataProvider, o => o.ValidateComplexTypesIfChildValidationFails = false);
// Act
var result = controller.TryValidateModel(model);
@ -213,8 +204,18 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private TestController CreateController(
ActionContext actionContext,
IModelMetadataProvider metadataProvider)
{
return CreateController(actionContext, metadataProvider, _ => { });
}
private TestController CreateController(
ActionContext actionContext,
IModelMetadataProvider metadataProvider,
Action<MvcOptions> optionsConfigurator
)
{
var options = actionContext.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>();
optionsConfigurator.Invoke(options.Value);
var controller = new TestController();
controller.ControllerContext = new ControllerContext(actionContext);
@ -249,37 +250,5 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class TestController : Controller
{
}
private class CustomObjectValidator : IObjectModelValidator
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IList<IModelValidatorProvider> _validatorProviders;
private ValidatorCache _validatorCache;
private CompositeModelValidatorProvider _validatorProvider;
public CustomObjectValidator(IModelMetadataProvider modelMetadataProvider, IList<IModelValidatorProvider> validatorProviders)
{
_modelMetadataProvider = modelMetadataProvider;
_validatorProviders = validatorProviders;
_validatorCache = new ValidatorCache();
_validatorProvider = new CompositeModelValidatorProvider(validatorProviders);
}
public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model)
{
var visitor = new ValidationVisitor(
actionContext,
_validatorProvider,
_validatorCache,
_modelMetadataProvider,
validationState);
var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType());
visitor.ValidateComplexTypesIfChildValidationFails = ValidateComplexTypesIfChildValidationFails;
visitor.Validate(metadata, prefix, model);
}
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using FormatterWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace FormatterWebSite
@ -84,5 +85,12 @@ namespace FormatterWebSite
{
return Ok();
}
[HttpPost]
[ModelStateValidationFilter]
public IActionResult CreateInvalidModel([FromBody] InvalidModel model)
{
return Ok(model);
}
}
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace FormatterWebSite.Models
{
public class InvalidModel : IValidatableObject
{
[Required]
public string Name { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
yield return new ValidationResult("The model is not valid.");
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace FormatterWebSite
{
public class StartupWithComplexParentValidation
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddControllers(options => options.ValidateComplexTypesIfChildValidationFails = true)
.AddNewtonsoftJson(options => options.SerializerSettings.Converters.Insert(0, new IModelConverter()))
.SetCompatibilityVersion(CompatibilityVersion.Latest);
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
}
}