Skip to content

Parameter binding

Every argument a handler takes is bound by code emitted for that handler's exact signature. The decisions are made during the build and written out as ordinary C#, so a parameter that cannot be bound is a build failure rather than a runtime one.

csharp
[Get("/mixed/{id}")]
public string Mixed(
    string id,                                  // the path token
    [FromQueryString] string filter,            // ?filter=
    [FromHeader("X-Tenant")] string tenant,     // a request header
    IMathService<int> mathService) {            // the container
    return $"{id}|{filter}|{tenant}|{mathService.Add(1, 2)}";
}

GET /mixed/id-9?filter=active with X-Tenant: acme yields id-9|active|acme|3.

The sources

SourceHow a parameter selects it
Path tokenThe parameter name matches a {token} in the route
Query string[FromQueryString], or [FromQueryString("q")] to name it
Header[FromHeader("X-Tenant")]
Cookie[FromCookie], or [FromCookie("session")] to name it
Form field[FromForm], or [FromForm("username")] to name it
Request body[FromBody], or inferred for a complex type with no other source
Container[FromServices], or inferred for a registered service type
CustomAn attribute implementing ICustomBindingAttribute

An unattributed parameter is resolved by elimination. A name matching a path token binds from the path, a type the container knows binds from the container, and what is left is the body.

csharp
[Post("/body/{label}")]
public string BodyWithPath(string label, MathAddModel model) => $"{label}:{model.Values.Count}";

Form fields

[FromForm] reads a field of an application/x-www-form-urlencoded body:

csharp
[Post("/sign-in")]
public IResult SignIn([FromForm] string username, [FromForm] string password) => ...;

It is explicit rather than inferred. A parameter the route does not declare binds from the body, and switching that to a form field whenever the content type happened to be a form would make a handler's binding depend on what the caller sent rather than on what the handler declared.

A handler cannot bind form fields and a body model at once — there is one body and the two readings are different. The generator reports that combination rather than leaving one of them to come back empty.

Fields only. multipart/form-data, which is what a form with a file input posts, is a different wire format and is not read by this.

Types

Path tokens, query values and headers arrive as strings and are converted to the declared type:

csharp
[Get("/path-typed/{count}")]
public int TypedPathToken(int count) => count * 2;

[Get("/query-typed")]
public int TypedQuery([FromQueryString] int page) => page + 1;

The body is deserialized as JSON into the parameter type. A value that fails to parse as its type answers 400 with the validation envelope.

Naming

[FromQueryString] and [FromHeader] bind by parameter name when given no argument, and by the supplied name otherwise. A header almost always needs the argument, because X-Tenant is not a C# identifier:

csharp
[Get("/query")]
public string ByParameterName([FromQueryString] string name) => name;          // ?name=

[Get("/query-named")]
public string ByAttributeName([FromQueryString("q")] string search) => search;  // ?q=

[Get("/header")]
public string Tenant([FromHeader("X-Tenant")] string tenant) => tenant;

Custom binding

An attribute implementing ICustomBindingAttribute takes over a parameter:

csharp
using Hardened.Requests.Abstract.Attributes;
using Hardened.Requests.Abstract.Execution;

public class TestFilterAttribute : Attribute, ICustomBindingAttribute {
    private readonly string _value;

    public TestFilterAttribute(string value) {
        _value = value;
    }

    public ValueTask<T> BindValue<T>(IExecutionContext context, IExecutionRequestParameter parameter) {
        if (typeof(T) == typeof(string)) {
            return new ValueTask<T>((T)(object)_value);
        }

        throw new NotSupportedException("Not supported");
    }
}
csharp
[Get("/test")]
public Task<string> TestValue([TestFilter("somevalue")] string testValue) =>
    Task.FromResult(testValue);

BindValue<T> is called with the parameter's declared type, which is why the example checks typeof(T). It receives the execution context, so it can read the request, the request-scoped service provider, or anything a filter earlier in the pipeline left behind. That is how the AWS package implements [NewImage] and [OldImage] on a DynamoDB stream handler: both read a record the pipeline put into the request scope.

What it looks like generated

Turn on EmitCompilerGeneratedFiles and the binding for the mixed handler above is a method that reads each source in order and calls your method. Nothing inspects ParameterInfo and nothing looks a name up in a dictionary of conventions.

Next

Released under the MIT License.