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();