|
| 1 | +# Parameter Binding |
| 2 | + |
| 3 | +**Level: Intermediate** 🍃🍃 |
| 4 | + |
| 5 | +Starting in .NET 6, route handlers (the Delegate parameters in [EndpointRouteBuilderExtensions](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) methods) can accept more than just an `HttpContext` as a parameter. Route handlers can now accept any number of parameters of different types bound from different sources. This guide describes the conventions that determine how each parameter is populated. |
| 6 | + |
| 7 | +## Conventions |
| 8 | + |
| 9 | +```cs |
| 10 | +app.MapPut("/todos/{id}", async (TodoDb db, TodoItem updateTodo, int id) => |
| 11 | +{ |
| 12 | + // ... |
| 13 | +}); |
| 14 | +``` |
| 15 | + |
| 16 | +The route handler above accepts three parameters: |
| 17 | + |
| 18 | +1. `TodoDb db` which comes from the service provider. |
| 19 | +2. `TodoItem updateTodo` which is read as JSON from the request body. |
| 20 | +3. `int id` which is read from the `{id}` segment of the route. |
| 21 | + |
| 22 | +These were all determined by convention, but could be specified explicitly with attributes as follows: |
| 23 | + |
| 24 | +```cs |
| 25 | +using Microsoft.AspNetCore.Mvc; |
| 26 | + |
| 27 | +// ... |
| 28 | +
|
| 29 | +app.MapPut("/todos/{id}", async ( |
| 30 | + [FromService] TodoDb db, |
| 31 | + [FromBody] TodoItem updateTodo, |
| 32 | + [FromRoute(Name = "id")] int nameDoesNotMatter) => |
| 33 | +{ |
| 34 | + // ... |
| 35 | +}); |
| 36 | +``` |
| 37 | + |
| 38 | +Parameters sources are determined using the following rules applied in order: |
| 39 | + |
| 40 | +1. [Parameter attributes](#attributes) take precedence over conventions if they are present. |
| 41 | +2. Any [well-known types](#well-known-types) are bound from the the [HttpContext](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext) or one of its properties. |
| 42 | +3. `string` parameters are bound from `HttpContext.RouteValues[{ParameterName}]` or `HttpContext.Query[{ParameterName}]` depending on whether `{ParameterName}` is part of the route pattern. |
| 43 | +4. Types with public static [BindAsync](#bindasync) methods are bound using `BindAsync`. |
| 44 | +5. Types with public static [TryParse](#tryparse) methods are bound by calling `TryParse` with `HttpContext.RouteValues[{ParameterName}]` or `HttpContext.Query[{ParameterName}]` depending on whether `{ParameterName}` is part of the route pattern. This includes most built-in numeric types, enums, `DateTime`, `TimeSpan` and more. |
| 45 | +6. Types registered as a service are bound from request services. |
| 46 | +7. Any remaining types are bound from the request body as JSON. |
| 47 | + |
| 48 | +## Attributes |
| 49 | + |
| 50 | +### `[FromRoute]` |
| 51 | + |
| 52 | +[FromRouteAttribute](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) implements `Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata`. Any attribute implementing this interface is equivalent. This will bind the parameter from `HttpRequest.RouteValues[{ParameterName}]`. If the parameter is not a string, the parameter type's [TryParse](#tryparse) method will be called to convert the string to the parameter type. |
| 53 | + |
| 54 | +If the `Name` property is provided (e.g. `[FromRoute(Name = "id")]`), the name specified using the property is used instead of the parameter name. |
| 55 | + |
| 56 | +### `[FromQuery]` |
| 57 | + |
| 58 | +[FromQueryAttribute](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute) implements `Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata`. Any attribute implementing this interface is equivalent. This will bind the parameter from `HttpRequest.Query[{ParameterName}]`. If the parameter is not a string, the parameter type's [TryParse](#tryparse) method will be called to convert the string to the parameter type. |
| 59 | + |
| 60 | +If the `Name` property is provided (e.g. `[FromQuery(Name = "page")]`), the name specified using the property is used instead of the parameter name. |
| 61 | + |
| 62 | +### `[FromHeader]` |
| 63 | + |
| 64 | +[FromHeaderAttribute](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute) implements `Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata`. Any attribute implementing this interface is equivalent. This will bind the parameter from `HttpRequest.Headers[{ParameterName}]`. If the parameter is not a string, the parameter type's [TryParse](#tryparse) method will be called to convert the string to the parameter type. |
| 65 | + |
| 66 | +If the `Name` property is provided (e.g. `[FromHeader(Name = "X-My-Custom-Header")]`), the name specified using the property is used instead of the parameter name. |
| 67 | + |
| 68 | +### `[FromServices]` |
| 69 | + |
| 70 | +[FromServicesAttribute](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) implements `Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata`. Any attribute implementing this interface is equivalent. This will bind the parameter from request services as described in the [Services section](#services) of this doc. |
| 71 | + |
| 72 | +### `[FromBody]` |
| 73 | + |
| 74 | +[FromBodyAttribute](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) implements `Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata`. Any attribute implementing this interface is equivalent. This will bind the parameter from the request body as JSON as described in the [JSON Request Body section](#json-request-body) of this doc. |
| 75 | + |
| 76 | +If the `EmptyBodyBehavior` property is set to `EmptyBodyBehavior.Allow` (e.g. `[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]`), the parameter will be set to `null` or its `default` value if the request body is empty. This corresponds to the `IFromBodyMetadata.AllowEmpty` being true. |
| 77 | + |
| 78 | +## Well-Known Types |
| 79 | + |
| 80 | +- HttpContext |
| 81 | +- HttpRequest (`HttpContext.Request`) |
| 82 | +- HttpResponse (`HttpContext.Response`) |
| 83 | +- ClaimsPrincipal (`HttpContext.User`) |
| 84 | +- CancellationToken (`HttpContext.RequestAborted`) |
| 85 | + |
| 86 | +## BindAsync |
| 87 | + |
| 88 | +If the parameter type, one of its parent/ancestor types or any of its implemented interfaces define a public static `BindAsync` method with one of the following signatures, the parameter will be bound using `BindAsync` assuming no parameter attribute specified another source. |
| 89 | + |
| 90 | +```cs |
| 91 | +public static ValueTask<{ParameterType}> BindAsync(HttpContext context, ParameterInfo parameter) |
| 92 | +{ |
| 93 | + // ... |
| 94 | +} |
| 95 | + |
| 96 | +// Or |
| 97 | +
|
| 98 | +public static ValueTask<{ParameterType}> BindAsync(HttpContext context) |
| 99 | +{ |
| 100 | + // ... |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +The return value can be either `ValueTask<{ParameterType}>` or `ValueTask<{ParameterType}?>` for both overloads. Whether returning a `null` value is allowed is determined by the nullability of parameter type. If the parameter type is non-nullable, the route handler will not be called and a bad request will be logged. |
| 105 | + |
| 106 | +In the case where both overloads are defined anywhere in the parameter type's hierarchy, the `BindAsync` method with the `ParameterInfo` argument will be called. |
| 107 | + |
| 108 | +If there is more than one `BindAsync` method with the same signature, the method from the most derived type will be called. `BindAsync` methods on interfaces are chosen last. A parameter type implementing more than one interface defining matching `BindAsync` methods is an error. |
| 109 | + |
| 110 | +## TryParse |
| 111 | + |
| 112 | +If the parameter type, one of its parent/ancestor types or any of its implemented interfaces define a public static `TryParse` method with one of the following signatures, the parameter will be bound using `TryParse` using the `string` from the source specified in the [Conventions section](#conventions) of this document. |
| 113 | + |
| 114 | +```cs |
| 115 | +public static bool TryParse(string? value, IFormatProvider formatProvider, out {ParameterType} result) |
| 116 | +{ |
| 117 | + // ... |
| 118 | +} |
| 119 | + |
| 120 | +// Or |
| 121 | +
|
| 122 | +public static bool TryParse(string? value, out {ParameterType} result) |
| 123 | +{ |
| 124 | + // ... |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +The out parameter can be either `out {ParameterType}` or `out {ParameterType}?` for both overloads. Whether providing a `null` value is allowed is determined by the nullability of parameter type. If the parameter type is non-nullable, the route handler will not be called and a bad request will be logged. |
| 129 | + |
| 130 | +In the case where both overloads are defined anywhere in the parameter type's hierarchy, the `TryParse` method with the `IFormatProvider` argument will be called. |
| 131 | + |
| 132 | +If there is more than one `TryParse` method with the same signature, the method from the most derived type will be called. `TryParse` methods on interfaces are chosen last. A parameter type implementing more than one interface defining matching `TryParse` methods is an error. |
| 133 | + |
| 134 | +## Services |
| 135 | + |
| 136 | +Service parameters are resolved from `HttpContext.RequestServices.GetService(typeof({ParameterType}))`. |
| 137 | + |
| 138 | +Whether or not a given parameter type is a service is determined using `IServiceProviderIsService` unless the parameter is explicitly attributed with `[FromServices]`. Given the `[FromServices]` attribute, the parameter type is assumed to exist. |
| 139 | + |
| 140 | +For non-nullable parameters, the parameter type must be resolvable as a service for the route handler to be called. If the service does not exist, an exception will be thrown when the endpoint is hit. For Nullable |
| 141 | + |
| 142 | +`IServiceProviderIsService` is a new interface introduced in .NET 6 that automatically implemented by the default service provider and some third-party containers. If `IServiceProviderIsService` itself is not available as a service, the `[FromServices]` attribute must be used to resolve parameters from services. |
| 143 | + |
| 144 | +## JSON Request Body |
| 145 | + |
| 146 | +JSON request bodies are read using the [ReadFromJsonAsync](https://docs.microsoft.com/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync#System_Net_Http_Json_HttpContentJsonExtensions_ReadFromJsonAsync__1_System_Net_Http_HttpContent_System_Text_Json_JsonSerializerOptions_System_Threading_CancellationToken_) extension method. This can be configured like all other calls to `ReadFromJsonAsync` using the [options pattern](https://docs.microsoft.com/aspnet/core/fundamentals/configuration/#configure-options-with-a-delegate-1) to configure [JsonOptions](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions). |
| 147 | + |
| 148 | +For non-nullable parameters, empty request bodies are disallowed by default. If a request matching the route pattern has an empty body, the route handler will not be called and a bad request will be logged. |
| 149 | + |
| 150 | +Empty request bodies are always allowed when the parameter is nullable even if `EmptyBodyBehavior.Disallow` is set via the `[FromBody]` attribute. |
0 commit comments