Skip to content

Add parameter binding documentation #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ module.exports = {
{
title: "Guide",
collapsable: true,
children: ["", "routing" ],
children: ["", "routing", "parameter-binding" ],
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion src/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ Level

Beginner 🍃

Intermidate 🍃🍃
Intermediate 🍃🍃

Advance 🍃🍃🍃
150 changes: 150 additions & 0 deletions src/guide/parameter-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Parameter Binding

**Level: Intermediate** 🍃🍃

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 with various types bound from different sources. This guide describes the conventions that determine how each parameter is populated.

## Conventions

```cs
app.MapPut("/todos/{id}", async (TodoDb db, TodoItem updateTodo, int id) =>
{
// ...
});
```

The route handler above accepts three parameters:

1. `TodoDb db` which comes from the service provider.
2. `TodoItem updateTodo` which is read as JSON from the request body.
3. `int id` which is read from the `{id}` segment of the route.

These were all determined by convention but could be specified explicitly with attributes as follows:

```cs
using Microsoft.AspNetCore.Mvc;

// ...

app.MapPut("/todos/{id}", async (
[FromService] TodoDb db,
[FromBody] TodoItem updateTodo,
[FromRoute(Name = "id")] int nameDoesNotMatter) =>
{
// ...
});
```

Parameters sources are determined using the following rules applied in order:

1. [Parameter attributes](#attributes) take precedence over conventions if they are present.
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.
3. `string` parameters are bound from `HttpContext.RouteValues[{ParameterName}]` or `HttpContext.Query[{ParameterName}]` depending on whether `{ParameterName}` is part of the route pattern.
4. Types with public static [BindAsync](#bindasync) methods are bound using `BindAsync`.
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.
6. Types registered as a service are bound from request services.
7. Any remaining types are bound from the request body as JSON.

## Attributes

### `[FromRoute]`

[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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once available, I want to link to API docs for the IFrom*Metadata interfaces.


If the `Name` property is provided (e.g. `[FromRoute(Name = "id")]`), the name specified using the property is used instead of the parameter name.

### `[FromQuery]`

[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.

If the `Name` property is provided (e.g. `[FromQuery(Name = "page")]`), the name specified using the property is used instead of the parameter name.

### `[FromHeader]`

[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.

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.

### `[FromServices]`

[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.

### `[FromBody]`

[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.

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.

## Well-Known Types

- HttpContext
- HttpRequest (`HttpContext.Request`)
- HttpResponse (`HttpContext.Response`)
- ClaimsPrincipal (`HttpContext.User`)
- CancellationToken (`HttpContext.RequestAborted`)

## BindAsync

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.

```cs
public static ValueTask<{ParameterType}> BindAsync(HttpContext context, ParameterInfo parameter)
{
// ...
}

// Or

public static ValueTask<{ParameterType}> BindAsync(HttpContext context)
{
// ...
}
```

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.

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.

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.

## TryParse

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.

```cs
public static bool TryParse(string? value, IFormatProvider formatProvider, out {ParameterType} result)
{
// ...
}

// Or

public static bool TryParse(string? value, out {ParameterType} result)
{
// ...
}
```

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.

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.

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.

## Services

Service parameters are resolved from `HttpContext.RequestServices.GetService(typeof({ParameterType}))`.

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.

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

[IServiceProviderIsService](https://docs.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice.isservice) 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.

## JSON Request Body

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).

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.

Empty request bodies are always allowed when the parameter is nullable even if `EmptyBodyBehavior.Disallow` is set via the `[FromBody]` attribute.