Skip to content

ApiExplorer loses top-level BinderModelName when expanding complex query parameters #68106

Description

@lorenzrox

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:

range.Start
range.End

or another equivalent representation that preserves the configured binding prefix.

Actual behavior

ApiExplorer generates:

Start
End

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:

created
updated

However, ApiExplorer exposes:

Start
End
Start
End

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:

Start
End

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:

Id
Name

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions