diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 86597714a310..8bd343d1c0a5 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Http.Metadata; @@ -633,11 +634,11 @@ private void Visit( // Order - source: Body // - // We don't want to append the **parameter** name when building a model name. + // Append the property name when building a model name, or the parameter name when it was explicitly provided via IModelNameProvider. var newContainerName = containerName; - if (modelMetadata.ContainerType != null) + if (modelMetadata.ContainerType != null || !string.IsNullOrEmpty(bindingContext.BinderModelName)) { - newContainerName = GetName(containerName, bindingContext); + newContainerName = GetName(containerName, bindingContext, source); } var metadataProperties = modelMetadata.Properties; @@ -674,7 +675,7 @@ private ApiParameterDescription CreateResult( return new ApiParameterDescription() { ModelMetadata = bindingContext.ModelMetadata, - Name = GetName(containerName, bindingContext), + Name = GetName(containerName, bindingContext, source), Source = source, Type = GetModelType(bindingContext.ModelMetadata), ParameterDescriptor = Parameter, @@ -693,10 +694,14 @@ private static Type GetModelType(ModelMetadata metadata) return metadata.ModelType; } - private static string GetName(string containerName, ApiParameterDescriptionContext metadata) + private static string GetName(string containerName, ApiParameterDescriptionContext metadata, BindingSource? source) { var propertyName = !string.IsNullOrEmpty(metadata.BinderModelName) ? metadata.BinderModelName : metadata.PropertyName; - return ModelNames.CreatePropertyModelName(containerName, propertyName); + Debug.Assert(propertyName is not null); + + return source == BindingSource.Header + ? propertyName + : ModelNames.CreatePropertyModelName(containerName, propertyName); } private readonly struct PropertyKey diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index a949d6cecc0b..21fd290c2b28 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -1624,11 +1624,33 @@ public void GetApiDescription_ParameterDescription_FromQueryEmployee() var description = Assert.Single(descriptions); Assert.Single(description.ParameterDescriptions); - var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Name"); + var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "employee.Name"); Assert.Same(BindingSource.Query, id.Source); Assert.Equal(typeof(string), id.Type); } + [Fact] + public void GetApiDescription_ParameterDescription_FromQueryEmployee_WithCustomPropertyBindingNames() + { + // Arrange + var action = CreateActionDescriptor(nameof(AcceptsEmployeeWithCustomPropertyNames)); + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + Assert.Equal(2, description.ParameterDescriptions.Count); + + var header = Assert.Single(description.ParameterDescriptions, p => p.Name == "X-MyCustomHeader"); + Assert.Same(BindingSource.Header, header.Source); + Assert.Equal(typeof(string), header.Type); + + var route = Assert.Single(description.ParameterDescriptions, p => p.Name == "employee.employeeid"); + Assert.Same(BindingSource.Path, route.Source); + Assert.Equal(typeof(string), route.Type); + } + [Fact] public void GetApiDescription_ParameterDescription_ParsablePrimitiveType() { @@ -2542,6 +2564,10 @@ private void AcceptsEmployee([FromQuery(Name = "employee")] Employee dto) { } + private void AcceptsEmployeeWithCustomPropertyNames([FromQuery(Name = "employee")] EmployeeWithCustomPropertyNames dto) + { + } + private void AcceptsTryParsablePrimitiveType([FromQuery] Guid id) { } @@ -2695,6 +2721,15 @@ private class Employee public string Name { get; set; } } + private class EmployeeWithCustomPropertyNames + { + [FromHeader(Name = "X-MyCustomHeader")] + public string HeaderName { get; set; } + + [FromRoute(Name = "employeeid")] + public string EmployeeId { get; set; } + } + [TypeConverter(typeof(EmployeeConverter))] private class ConvertibleEmployee {