Skip to content

Support multiple [From...] attributes on the same member: convert to CompositeBindingSource #62809

@julealgon

Description

@julealgon

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I created a custom binding source that represents the current request's host:

public static class ExtendedBindingSources
{
    /// <summary>
    /// A <see cref="BindingSource" /> for the request host.
    /// </summary>
    public static readonly BindingSource Host = new(
        "Host",
        "A binding source that fetches data from the request's host",
        isGreedy: false,
        isFromRequest: true);
}

I then created my own [FromHost] attribute, inheriting from IBindingSourceMetadata, to indicate that a member should fetch from that source. After, I created an associated HostValueProvider, inheriting from BindingSourceValueProvider, that is responsible for actually fetching the host value. Because the host doesn't vary per input, and is a single value, this provider always provides the same value no matter the name or prefix of the inputs:

public class HostValueProvider(HttpRequest httpRequest) : BindingSourceValueProvider(ExtendedBindingSources.Host)
{
    public override bool ContainsPrefix(string prefix) => true;

    public override ValueProviderResult GetValue(string key) => new(httpRequest.Host.Host);
}

However, in my specific use case, I only want to fetch the value from the host if the value is not available from the route. Since I registered my HostValueProvider + HostValueProviderFactory in MvcOptions as the last factory, it would be called after all other value providers were exhausted. This allows me to naturally "fallback" to the host value if other sources did not provide a value for the member/parameter.

This works perfectly fine mechanically and I'm quite happy with how clean it ended up, but there is a limitation I ran into with regards to how the [From...] attributes work.

At first, I tried something like this:

[FromRoute][FromHost] myParameter

My hope was that with multiple [From...] attributes, the framework would consider all of them when going through the associated value sources.

But I quickly realized that when multiple such parameters are specified, only the first one is honored. In the example above, even when the route did not have a value for myParameter, it would not try to fetch the value from my custom host provider.

If I inverted the attribute order:

[FromHost][FromRoute] myParameter

It would then only fetch the value from my custom host provider, and the value on the route would be ignored, even if present.

To work around this, I created a somewhat convoluted custom attribute to combine 2 sources into a single CompositeBindingSource:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromMultipleAttribute<TSource1, TSource2>() : FromMultipleAttribute(new TSource1(), new TSource2())
    where TSource1 : IBindingSourceMetadata, new()
    where TSource2 : IBindingSourceMetadata, new();

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromMultipleAttribute(params IBindingSourceMetadata[] bindingSources) : Attribute, IBindingSourceMetadata
{
    /// <inheritdoc />
    public BindingSource BindingSource { get; } = CompositeBindingSource.Create(
        bindingSources.Select(s => s.BindingSource),
        string.Join(',', bindingSources.Select(s => s.BindingSource.DisplayName)));
}

Which then allowed me to do this:

[FromMultiple<FromRouteAttribute, FromHostAttribute>] myParameter

Which works perfectly: both the host and the route are now considered as sources for the value, in the order I would expect (first the route is checked, then the host, based on the order that the value provider factories are registered, which is exactly how it was designed to work).

I would love for this to work more naturally instead though. It should be fairly straightforward for the framework to detect and combine multiple IBindingSourceMetadata attributes into a single CompositeBindingSource automatically, which would remove the need for this somewhat complicated (and limiting) combined attribute, and allow me to just do what I initially tried:

[FromRoute][FromHost] myParameter

Describe the solution you'd like

AspNetCore MVC should see multiple [From...] attributes on a member, and respect all of them by combining their binding sources.

I would like to just specify the attributes like this:

[FromRoute][FromQuery] myParameter

And this should mean that either the route or the queryString could provide values for my argument.

The behavior today is that the first such attribute completely overrides the mechanism and makes all other subsequent attributes irrelevant, which I think is very unintuitive behavior since adding multiple attributes also doesn't produce any runtime errors.

Additional context

I'm not sure if the logic would need to take special care about BindingSource values with the isGreedy flag turned on. This is currently outside of my use case as both sources in my scenario are not greedy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions