diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs index eadaa965ded5..e450f69eb275 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs @@ -364,7 +364,7 @@ internal void CreateModel(ModelBindingContext bindingContext) } var fieldName = property.BinderModelName ?? property.PropertyName!; - var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName); + var modelName = ModelNames.CreatePropertyModelNameOptimized(bindingContext.ModelName, fieldName); var result = await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName); if (result.IsModelSet) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/ModelNames.cs b/src/Mvc/Mvc.Core/src/ModelBinding/ModelNames.cs index 8e6afac46e68..c9d2f3cdffd1 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/ModelNames.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/ModelNames.cs @@ -61,4 +61,44 @@ public static string CreatePropertyModelName(string? prefix, string? propertyNam return prefix + "." + propertyName; } + + /// + /// Create a model property name using a prefix and a property name, + /// with a small optimization to avoid redundancy. + /// + /// For example, if both and are "parameter" + /// (ignoring case), the result will be just "parameter" instead of "parameter.Parameter". + /// + /// The prefix to use. + /// The property name. + /// The property model name. + public static string CreatePropertyModelNameOptimized(string? prefix, string? propertyName) + { + if (string.IsNullOrEmpty(prefix)) + { + return propertyName ?? string.Empty; + } + + if (string.IsNullOrEmpty(propertyName)) + { + return prefix ?? string.Empty; + } + + if (propertyName.StartsWith('[')) + { + // The propertyName might represent an indexer access, in which case combining + // with a 'dot' would be invalid. This case occurs only when called from ValidationVisitor. + return prefix + propertyName; + } + + if (string.Equals(prefix, propertyName, StringComparison.OrdinalIgnoreCase)) + { + // if we are dealing with with something like: + // prefix = parameter and propertyName = parameter + // it should fallback to the property name. + return propertyName; + } + + return prefix + "." + propertyName; + } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 08c1f5142578..d8a202f54030 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -6,3 +6,4 @@ Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute.Description.get -> Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute.Description.set -> void Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.Description.get -> string? Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.Description.set -> void +static Microsoft.AspNetCore.Mvc.ModelBinding.ModelNames.CreatePropertyModelNameOptimized(string? prefix, string? propertyName) -> string! diff --git a/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs index 8bb72ff67055..591618788c9e 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs @@ -338,6 +338,74 @@ public async Task CollectionModelBinder_BindsListOfComplexType_WithRequiredPrope Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage); } + class TestClass + { + public TestClass2 Parameter { get; set; } + } + + class TestClass2 + { + public string Parameter { get; set; } = ""; + } + + [Fact(Skip = "Nested properties are more complex to deal with it. See https://github.com/dotnet/aspnetcore/pull/62459 for more info.")] + public async Task CollectionModelBinder_CanBind_NestedProperties_WithSameNameOfParameter() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(TestClass) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString("?parameter.parameter=testing"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("testing", model.Parameter.Parameter); + Assert.True(modelState.IsValid); + } + + [Fact] + public async Task CollectionModelBinder_CanBind_SimpleProperties_WithSameNameOfParameter() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(TestClass2) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString("?parameter=testing"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("testing", model.Parameter); + Assert.True(modelState.IsValid); + } + [Fact] public async Task CollectionModelBinder_BindsListOfComplexType_WithRequiredProperty_WithExplicitPrefix_PartialData() {