Is there an existing issue for this?
I searched the existing issues and found related discussions (for example #26271, #43464), but I could not find one describing this specific behavior.
Describe the bug
ApiExplorer loses the top-level BinderModelName when expanding a complex [FromQuery] parameter into individual ApiParameterDescription entries.
As a result, the generated ApiDescription does not fully represent the parameter binding configuration defined by MVC.
This becomes particularly problematic for consumers of ApiDescription (for example OpenAPI generators such as Swashbuckle), because the generated parameter descriptions lose the correlation between nested properties and their originating top-level parameter.
To Reproduce
public sealed class SearchQuery
{
public DateTimeOffset? Start { get; set; }
public DateTimeOffset? End { get; set; }
}
[HttpGet]
public IActionResult Get([FromQuery(Name = "range")] SearchQuery query)
{
return Ok();
}
Inspect the generated ApiDescription.
Expected behavior
The generated parameter descriptions should preserve the configured model name.
For example:
or another equivalent representation that preserves the configured binding prefix.
Actual behavior
ApiExplorer generates:
The configured top-level BinderModelName is not preserved in the generated parameter descriptions.
Why this is problematic
The generated ApiDescription does not faithfully represent the parameter configuration defined by MVC.
The following two actions:
[HttpGet]
public IActionResult Get1([FromQuery] SearchQuery query)
{
return Ok();
}
[HttpGet]
public IActionResult Get2([FromQuery(Name = "range")] SearchQuery query)
{
return Ok();
}
result in effectively identical nested parameter descriptions, despite having different binding configurations.
As a consequence, consumers of ApiDescription cannot determine whether a custom model name was configured through IModelNameProvider.
Loss of parameter identity
This issue becomes particularly problematic when multiple complex parameters contain nested properties with the same names.
Consider the following example:
public sealed class DateRange
{
public DateTimeOffset? Start { get; set; }
public DateTimeOffset? End { get; set; }
}
[HttpGet]
public IActionResult Get([FromQuery(Name = "created")] DateRange createdRange, [FromQuery(Name = "updated")] DateRange updatedRange)
{
return Ok();
}
The two top-level parameters have distinct configured model names:
However, ApiExplorer exposes:
At this point the identity of the originating parameter has been lost.
Consumers of ApiDescription can no longer determine which Start and End properties belong to createdRange and which belong to updatedRange, even though the corresponding BinderModelName values are available on the original action parameters.
This information loss makes it impossible for downstream consumers to reliably reconstruct the original binding contract.
Additional investigation
The parameter metadata is correct.
Using:
var metadata = metadataProvider.GetMetadataForParameter(parameterInfo);
returns:
BinderModelName = "range"
Model binding is also correct:
bindingContext.ModelName == "range"
The issue appears to originate in DefaultApiDescriptionProvider.PseudoModelBindingVisitor.
The traversal starts with:
Visit(context, source, containerName: string.Empty);
where the root ApiParameterDescriptionContext still contains:
BinderModelName = range
PropertyName = query
ContainerType = null
Property names are later generated through:
private static string GetName(string containerName, ApiParameterDescriptionContext metadata)
{
var propertyName = !string.IsNullOrEmpty(metadata.BinderModelName) ? metadata.BinderModelName : metadata.PropertyName;
return ModelNames.CreatePropertyModelName(containerName, propertyName);
}
However, child property contexts are created from property metadata:
var propertyContext = new ApiParameterDescriptionContext(
propertyMetadata,
bindingInfo,
propertyName: null);
At that point the original top-level BinderModelName is no longer available.
As a result, the generated names become:
instead of preserving the configured parameter identity.
Why this matters
The generated ApiDescription loses information that is explicitly configured through MVC model binding.
Although model binding itself behaves correctly, the resulting ApiDescription is no longer sufficient to distinguish:
[FromQuery] SearchQuery query
from:
[FromQuery(Name = "range")] SearchQuery query
nor is it sufficient to associate nested properties with their originating top-level parameter when multiple complex parameters expose properties with the same names.
Further evidence
The same behavior can be reproduced with any complex type and is not specific to custom model binders or custom metadata providers:
public sealed class MyModel
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
[HttpGet]
public IActionResult Get([FromQuery(Name = "myprefix")] MyModel model)
{
return Ok();
}
ApiExplorer exposes:
while the parameter metadata still contains:
BinderModelName = myprefix
.NET Version
Observed on:
- .NET 8.0.423
- .NET 10.0.302
Additional context
This appears to be an ApiExplorer-specific issue.
The configured model name is preserved by MVC model metadata and by model binding itself, but is lost during PseudoModelBindingVisitor traversal when generating ApiParameterDescription instances.
Is there an existing issue for this?
I searched the existing issues and found related discussions (for example #26271, #43464), but I could not find one describing this specific behavior.
Describe the bug
ApiExplorer loses the top-level
BinderModelNamewhen expanding a complex[FromQuery]parameter into individualApiParameterDescriptionentries.As a result, the generated
ApiDescriptiondoes not fully represent the parameter binding configuration defined by MVC.This becomes particularly problematic for consumers of
ApiDescription(for example OpenAPI generators such as Swashbuckle), because the generated parameter descriptions lose the correlation between nested properties and their originating top-level parameter.To Reproduce
Inspect the generated
ApiDescription.Expected behavior
The generated parameter descriptions should preserve the configured model name.
For example:
or another equivalent representation that preserves the configured binding prefix.
Actual behavior
ApiExplorer generates:
The configured top-level
BinderModelNameis not preserved in the generated parameter descriptions.Why this is problematic
The generated
ApiDescriptiondoes not faithfully represent the parameter configuration defined by MVC.The following two actions:
result in effectively identical nested parameter descriptions, despite having different binding configurations.
As a consequence, consumers of
ApiDescriptioncannot determine whether a custom model name was configured throughIModelNameProvider.Loss of parameter identity
This issue becomes particularly problematic when multiple complex parameters contain nested properties with the same names.
Consider the following example:
The two top-level parameters have distinct configured model names:
However, ApiExplorer exposes:
At this point the identity of the originating parameter has been lost.
Consumers of
ApiDescriptioncan no longer determine whichStartandEndproperties belong tocreatedRangeand which belong toupdatedRange, even though the correspondingBinderModelNamevalues are available on the original action parameters.This information loss makes it impossible for downstream consumers to reliably reconstruct the original binding contract.
Additional investigation
The parameter metadata is correct.
Using:
returns:
Model binding is also correct:
The issue appears to originate in
DefaultApiDescriptionProvider.PseudoModelBindingVisitor.The traversal starts with:
where the root
ApiParameterDescriptionContextstill contains:Property names are later generated through:
However, child property contexts are created from property metadata:
At that point the original top-level
BinderModelNameis no longer available.As a result, the generated names become:
instead of preserving the configured parameter identity.
Why this matters
The generated
ApiDescriptionloses information that is explicitly configured through MVC model binding.Although model binding itself behaves correctly, the resulting
ApiDescriptionis no longer sufficient to distinguish:from:
nor is it sufficient to associate nested properties with their originating top-level parameter when multiple complex parameters expose properties with the same names.
Further evidence
The same behavior can be reproduced with any complex type and is not specific to custom model binders or custom metadata providers:
ApiExplorer exposes:
while the parameter metadata still contains:
.NET Version
Additional context
This appears to be an ApiExplorer-specific issue.
The configured model name is preserved by MVC model metadata and by model binding itself, but is lost during
PseudoModelBindingVisitortraversal when generatingApiParameterDescriptioninstances.