Skip to content

Allow ValidationVisitor.ValidateComplexTypesIfChildValidationFails to be configured via MvcOptions #9312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ public MvcOptions() { }
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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public override ValidationVisitor GetValidationVisitor(
validationState)
{
MaxValidationDepth = _mvcOptions.MaxValidationDepth,
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
};

return visitor;
Expand Down
10 changes: 10 additions & 0 deletions src/Mvc/Mvc.Core/src/MvcOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ public int? MaxValidationDepth
}
}

/// <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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,33 @@ public void Validate_Throws_WithMaxDepth_1()
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()
{
Expand Down Expand Up @@ -1522,6 +1549,22 @@ private class ValidatableModelContainer
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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Text;
using System.Threading.Tasks;
using FormatterWebSite;
using FormatterWebSite.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;
Expand Down
132 changes: 132 additions & 0 deletions src/Mvc/test/Mvc.FunctionalTests/InputParentValidationTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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;
Expand Down Expand Up @@ -141,11 +140,7 @@ public void ValidationVisitor_ValidateComplexTypesIfChildValidationFailsSetToTru
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);
Expand All @@ -166,11 +161,7 @@ public void ValidationVisitor_ValidateComplexTypesIfChildValidationFailsSetToFal
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);
Expand Down Expand Up @@ -213,8 +204,18 @@ private void AssertErrorEquals(string expected, string actual)
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);
Expand Down Expand Up @@ -249,37 +250,5 @@ private Dictionary<string, string> GetModelStateErrors(ModelStateDictionary mode
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; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,5 +85,12 @@ public IActionResult ValidationThrowsError_WhenValidationExceedsMaxValidationDep
{
return Ok();
}

[HttpPost]
[ModelStateValidationFilter]
public IActionResult CreateInvalidModel([FromBody] InvalidModel model)
{
return Ok(model);
}
}
}
16 changes: 16 additions & 0 deletions src/Mvc/test/WebSites/FormatterWebSite/Models/InvalidModel.cs
Original file line number Diff line number Diff line change
@@ -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.");
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
}