Skip to content

Infer default sources for parameters in MapAction #30248

Closed
@halter73

Description

@halter73

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.

  1. 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.
  2. 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
  3. 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.
  4. 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.

  1. 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.
  2. 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:

  1. For parameters bound from the body, we already call HttpRequest.ReadFromJsonAsync<{ParameterType}>().
  2. All other parameters start as strings from the various sources listed above.
    1. If {ParameterType} is string, we're done!
    2. If there's a {ParameterType}.Parse({ValueString}) method, use that.
      • NOTE: Going forward, prefer/require TryParse instead.
    3. Lastly, try Convert.ChangeType({ValueString}, typeof{ParameterType}, CultureInfo.InvariantCulture)
      • NOTE: Going forward, we should stop using CultureInfo.InvariantCulture

Original issue:

  1. Complex type -> FromBody (See https://source.dot.net/#Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs,606)
  2. Name appears as a route value -> FromRoute
  3. Everything else -> FromQuery

From: #29878 (comment)

New inference rules unique to MapAction:

  1. [FromServices] should work implicitly for types that can be resolved from RequestServices.

Metadata

Metadata

Assignees

Labels

area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcenhancementThis issue represents an ask for new feature or an enhancement to an existing onefeature-minimal-actionsController-like actions for endpoint routing

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions