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:
parent
77424a6b06
commit
2d33c32187
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
validationState)
|
||||
{
|
||||
MaxValidationDepth = _mvcOptions.MaxValidationDepth,
|
||||
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
|
||||
};
|
||||
|
||||
return visitor;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue