From 556880872d72e9a38a48283567ed021db08f2905 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Thu, 26 Jul 2018 12:22:56 -0700 Subject: [PATCH] Ensure later validations of `null` models do not overwrite `Invalid` state - #8078 --- .../Validation/ValidationVisitor.cs | 6 ++- .../ModelBinding/ParameterBinderTest.cs | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index ecf44973cc..43c385ec7b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -80,6 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation /// Indicates whether validation of a complex type should be performed if validation fails for any of its children. The default behavior is false. /// public bool ValidateComplexTypesIfChildValidationFails { get; set; } + /// /// Validates a object. /// @@ -105,7 +106,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation if (model == null && key != null && !alwaysValidateAtTopLevel) { var entry = ModelState[key]; - if (entry != null && entry.ValidationState != ModelValidationState.Valid) + + // Rationale: We might see the same model state key for two different objects and want to preserve any + // known invalidity. + if (entry != null && entry.ValidationState != ModelValidationState.Invalid) { entry.ValidationState = ModelValidationState.Valid; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs index 8b7ea899ed..5b67a06b92 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs @@ -836,6 +836,60 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding }); } + // Regression test for aspnet/Mvc#8078. Later parameter should not mark entry as valid. + [Fact] + public async Task BindModelAsync_ForOverlappingParameters_InValid_WithInValidFirstParameterAndSecondNull() + { + // Arrange + var parameterDescriptor = new ParameterDescriptor + { + BindingInfo = new BindingInfo + { + BinderModelName = "id", + }, + Name = "identifier", + ParameterType = typeof(string), + }; + + var actionContext = GetControllerContext(); + var modelState = actionContext.ModelState; + + // Mimic ModelStateEntry when first parameter is [FromRoute] int id and request URI is /api/values/notAnInt + modelState.SetModelValue("id", "notAnInt", "notAnInt"); + modelState.AddModelError("id", "This is not valid."); + + var modelMetadataProvider = new TestModelMetadataProvider(); + var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(string)); + var parameterBinder = new ParameterBinder( + modelMetadataProvider, + Mock.Of(), + new DefaultObjectValidator( + modelMetadataProvider, + new[] { TestModelValidatorProvider.CreateDefaultProvider() }), + _optionsAccessor, + NullLoggerFactory.Instance); + + // Mimic result when second parameter is [FromQuery(Name = "id")] string identifier and query is ?id + var modelBindingResult = ModelBindingResult.Success(null); + var modelBinder = CreateMockModelBinder(modelBindingResult); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + modelBinder, + new SimpleValueProvider(), + parameterDescriptor, + modelMetadata, + value: null); + + // Assert + Assert.True(result.IsModelSet); + Assert.False(modelState.IsValid); + var keyValuePair = Assert.Single(modelState); + Assert.Equal("id", keyValuePair.Key); + Assert.Equal(ModelValidationState.Invalid, keyValuePair.Value.ValidationState); + } + private static ControllerContext GetControllerContext() { var services = new ServiceCollection();