How to determine each parameter's source
The numbered and lettered rules for determining the source of a parameter should be applied in the order. The bullet points are unordered.
- If an attribute that implements one of the "IFrom..." interfaces in
Microsoft.AspNetCore.Http.Metadata is applied to the parameter, populate the parameter from the source specified by the attribute. Only one of these attributes can be specified per parameter, and these attributes must implement no more than one of these interfaces.
IFromRouteMetadata
- Reads from
HttpRequest.RouteValues[name] where name is specified by IFromRouteMetadata.Name or the parameter name if IFromRouteMetadata.Name is null.
IFromBodyMetadata
- Reads from
HttpRequest.Body(Reader) as JSON using HttpRequest.ReadFromJsonAsync<{ParameterType}>().
- Can only be used once per parameter list (there's only one body)
- Cannot be used in the same parameter list as
IFromFormMetadata
- Will cause requests with an empty body to be rejected without calling the action unless
IFromBodyMetadata.AllowEmpty returns true. (The default interface implementation returns false.)
IFromFormMetadata
- Reads from
HttpRequest.Form[name] where name is specified by IFromFormMetadata.Name or the parameter name if IFromFormMetadata.Name is null.
- Can be used multiple times in a single parameter list (presumably for different names)
- Cannot be used in the same parameter list as
IFromBodyMetadata (there's only one body)
IFromHeaderMetadata
- Reads from
HttpRequest.Headers[name] where name is specified by IFromHeaderMetadata.Name or the parameter name if IFromHeaderMetadata.Name is null.
IFromQueryMetadata
- Reads from
HttpRequest.Query[name] where name is specified by IFromQueryMetadata.Name or the parameter name if IFromQueryMetadata.Name is null.
IFromServiceMetadata
- Normally resolves via
HttpRequest.RequestServices.GetRequiredService<{ParameterType}>().
- If the parameter is a nullable reference type (i.e. has the
System.Runtime.CompilerServices.NullableAttribute), resolve via HttpRequest.RequestServices.GetService<{ParameterType}>().
- If the parameter is optional (i.e. has a default value), resolve via
HttpRequest.RequestServices.GetService<{ParameterType}>() ?? parameter.DefaultValue.
- If the parameter is a scalar value type that lives in the System namespace, first check
HttpRequest.RouteValues contains an entry for the parameter name. If it does, use HttpRequest.RouteValues[{ParameterName}]. Otherwise, use HttpRequest.Query[{ParameterName}].
- TODO: Come up with actual list of scalar value types that live in the System namespace.
- Question: Do we want to allow types to opt-in to this? Today MVC uses
TypeConverter which is hard to implement.
- Alternatives:
- Something convention-based like the presence of a
bool TryParse(string, out T) method
- The explicit implementation of an interface with that method to make it more opt in
- If
HttpRequest.RequestServices.GetService<{ParameterType}>() is not null, use that value.
- TODO: Figure out how to make this work well with compile-time source generation. This might need to be determined at runtime no matter what.
- Otherwise, treat the parameter as if it had an attribute implementing
IFromBodyMetadata where IFromBodyMetadata.AllowEmpty is false.
How we will convert to the parameter type.
- For parameters bound from the body (has an
IFromBodyMetadata attribute or is treated by such by convention), we will continue to assume JSON and initialize the parameter with HttpRequest.ReadFromJsonAsync<{ParameterType}>().
- We need to figure out if we want make this configurable or force people to read they body themselves if they want configurability. If we decide on configurability, we need to decide on how much.
- For all other parameters bound from the request (i.e. not a service or from the body) we need to convert from a string.
- Since there is a finite list of "scalar value who lives in the System namespace" we'll bind to by convention, we'll use their TryParse methods or equivalent until we decide on something different for those.
- Parameters explicitly bound by attribute to a string-based source also need to be converted much like those read from the
RouteValues or Query by convention. I doubt we also want to limit these parameter types to a finite list. For now, we'll continue doing what we do today to convert from string as described below.
How we convert to the parameter type today in the "MapAction" APIs
Today, all inputs are converted to their respective parameter types as follows:
- For parameters bound from the body, we already call
HttpRequest.ReadFromJsonAsync<{ParameterType}>().
- All other parameters start as
strings from the various sources listed above.
- If
{ParameterType} is string, we're done!
- If there's a
{ParameterType}.Parse({ValueString}) method, use that.
- NOTE: Going forward, prefer/require
TryParse instead.
- Lastly, try
Convert.ChangeType({ValueString}, typeof{ParameterType}, CultureInfo.InvariantCulture)
- NOTE: Going forward, we should stop using
CultureInfo.InvariantCulture
Original issue:
- Complex type -> FromBody (See https://source.dot.net/#Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs,606)
- Name appears as a route value -> FromRoute
- Everything else -> FromQuery
From: #29878 (comment)
New inference rules unique to MapAction:
[FromServices] should work implicitly for types that can be resolved from RequestServices.
How to determine each parameter's source
The numbered and lettered rules for determining the source of a parameter should be applied in the order. The bullet points are unordered.
Microsoft.AspNetCore.Http.Metadatais applied to the parameter, populate the parameter from the source specified by the attribute. Only one of these attributes can be specified per parameter, and these attributes must implement no more than one of these interfaces.IFromRouteMetadataHttpRequest.RouteValues[name]wherenameis specified byIFromRouteMetadata.Nameor the parameter name ifIFromRouteMetadata.Nameis null.IFromBodyMetadataHttpRequest.Body(Reader)as JSON usingHttpRequest.ReadFromJsonAsync<{ParameterType}>().IFromFormMetadataIFromBodyMetadata.AllowEmptyreturns true. (The default interface implementation returns false.)IFromFormMetadataHttpRequest.Form[name]wherenameis specified byIFromFormMetadata.Nameor the parameter name ifIFromFormMetadata.Nameis null.IFromBodyMetadata(there's only one body)IFromHeaderMetadataHttpRequest.Headers[name]wherenameis specified byIFromHeaderMetadata.Nameor the parameter name ifIFromHeaderMetadata.Nameis null.IFromQueryMetadataHttpRequest.Query[name]wherenameis specified byIFromQueryMetadata.Nameor the parameter name ifIFromQueryMetadata.Nameis null.IFromServiceMetadataHttpRequest.RequestServices.GetRequiredService<{ParameterType}>().System.Runtime.CompilerServices.NullableAttribute), resolve viaHttpRequest.RequestServices.GetService<{ParameterType}>().HttpRequest.RequestServices.GetService<{ParameterType}>() ?? parameter.DefaultValue.HttpRequest.RouteValuescontains an entry for the parameter name. If it does, useHttpRequest.RouteValues[{ParameterName}]. Otherwise, useHttpRequest.Query[{ParameterName}].TypeConverterwhich is hard to implement.bool TryParse(string, out T)methodHttpRequest.RequestServices.GetService<{ParameterType}>()is not null, use that value.IFromBodyMetadatawhereIFromBodyMetadata.AllowEmptyis false.How we will convert to the parameter type.
IFromBodyMetadataattribute or is treated by such by convention), we will continue to assume JSON and initialize the parameter withHttpRequest.ReadFromJsonAsync<{ParameterType}>().RouteValuesorQueryby convention. I doubt we also want to limit these parameter types to a finite list. For now, we'll continue doing what we do today to convert from string as described below.How we convert to the parameter type today in the "MapAction" APIs
Today, all inputs are converted to their respective parameter types as follows:
HttpRequest.ReadFromJsonAsync<{ParameterType}>().strings from the various sources listed above.{ParameterType}isstring, we're done!{ParameterType}.Parse({ValueString})method, use that.TryParseinstead.Convert.ChangeType({ValueString}, typeof{ParameterType}, CultureInfo.InvariantCulture)CultureInfo.InvariantCultureOriginal issue: