diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs index 0c8834871aeb..ed4d90e2e4f0 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs @@ -487,6 +487,14 @@ protected virtual object CreateModel(ModelBindingContext bindingContext) var modelType = bindingContext.ModelType; if (modelType.IsAbstract || modelType.GetConstructor(Type.EmptyTypes) == null) { + // If the model is not a top-level object, we can't examine the defined constructor + // to evaluate if the non-null property has been set so we do not provide this as a valid + // alternative. + if (!bindingContext.IsTopLevelObject) + { + throw new InvalidOperationException(Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForType(modelType.FullName)); + } + var metadata = bindingContext.ModelMetadata; switch (metadata.MetadataKind) { diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs index d1da6f7c7b1f..d1c65c0cd497 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs @@ -588,11 +588,8 @@ public void CreateModel_ForStructModelType_AsProperty_ThrowsException() string.Format( CultureInfo.CurrentCulture, "Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " + - "value types and must have a parameterless constructor. Alternatively, set the '{1}' property to" + - " a non-null value in the '{2}' constructor.", - typeof(PointStruct).FullName, - nameof(Location.Point), - typeof(Location).FullName), + "value types and must have a parameterless constructor.", + typeof(PointStruct).FullName), exception.Message); } @@ -1002,6 +999,32 @@ public async Task BindModelAsync_Success() Assert.True(bindingContext.ModelState.IsValid); } + // Validates fix for https://github.com/dotnet/aspnetcore/issues/21916 + [Fact] + public async Task BindModelAsync_PropertyInitializedInNonParameterlessConstructorConstructor() + { + // Arrange + var model = new ModelWithPropertyInitializedInConstructor("TestName"); + var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithPropertyInitializedInConstructor.NameContainer)); + var nestedProperty = GetMetadataForProperty(typeof(ClassWithNoParameterlessConstructor), nameof(ClassWithNoParameterlessConstructor.Name)); + var bindingContext = CreateContext(property); + bindingContext.IsTopLevelObject = false; + var valueProvider = new Mock(MockBehavior.Strict); + valueProvider + .Setup(provider => provider.ContainsPrefix("theModel.Name")) + .Returns(true); + bindingContext.ValueProvider = valueProvider.Object; + var binder = CreateBinder(bindingContext.ModelMetadata); + + binder.Results[nestedProperty] = ModelBindingResult.Success(null); + + // Act + var exception = await Assert.ThrowsAsync(async () => await binder.BindModelAsync(bindingContext)); + // Assert + var unexpectedMessage = "Alternatively, set the 'NameContainer' property to a non-null value in the 'Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderTest+ModelWithPropertyInitializedInConstructor' constructor."; + Assert.DoesNotContain(exception.Message, unexpectedMessage); + } + [Fact] public void SetProperty_PropertyHasDefaultValue_DefaultValueAttributeDoesNothing() { @@ -1302,6 +1325,17 @@ public ClassWithNoParameterlessConstructor(string name) public string Name { get; set; } } + private class ModelWithPropertyInitializedInConstructor + { + public ModelWithPropertyInitializedInConstructor(string name) + { + NameContainer = new ClassWithNoParameterlessConstructor(name); + } + + [ValueBinderMetadataAttribute] + public ClassWithNoParameterlessConstructor NameContainer { get; set; } + } + private class BindingOptionalProperty { [BindingBehavior(BindingBehavior.Optional)] diff --git a/src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs index 2d1fc94523d1..a1f93dfd9acf 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs @@ -478,11 +478,8 @@ public async Task ActionParameter_UsingComplexTypeModelBinder_ModelPropertyTypeW string.Format( CultureInfo.CurrentCulture, "Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " + - "value types and must have a parameterless constructor. Alternatively, set the '{1}' property to" + - " a non-null value in the '{2}' constructor.", - typeof(ClassWithNoDefaultConstructor).FullName, - nameof(Class1.Property1), - typeof(Class1).FullName), + "value types and must have a parameterless constructor.", + typeof(ClassWithNoDefaultConstructor).FullName), exception.Message); }