Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Using AsParameters 3.13

WARNING

When you use [AsParameters], you can read HTTP form data or deserialize a request body as JSON, but not both at the same time and Wolverine will happily throw an exception telling you so if you try to do this.

TIP

Use Wolverine's pre-generated code to understand exactly how Wolverine is processing any model object decorated with [AsParameters]

Wolverine supports the ASP.Net Core AsParameters attribute usage for complex binding of a mixed bag of HTTP information including headers, form data elements, route arguments, the request body, IoC services to a single input model using the ASP.Net Core [AsParameters] attribute as a marker.

See the Microsoft documentation on AsParameters for more background.

Below is a sample from our test suite showing what is possible for query string and header values:

cs
public static class AsParametersEndpoints{
    [WolverinePost("/api/asparameters1")]
    public static AsParametersQuery Post([AsParameters] AsParametersQuery query)
    {
        return query;
    }
}

public class AsParametersQuery{
    [FromQuery]
    public Direction EnumFromQuery{ get; set; }
    [FromForm]
    public Direction EnumFromForm{ get; set; }

    public Direction EnumNotUsed{get;set;}

    [FromQuery]
    public string StringFromQuery { get; set; } = null!;
    [FromForm]
    public string StringFromForm { get; set; } = null!;
    public string StringNotUsed { get; set; } = null!;
    [FromQuery]
    public int IntegerFromQuery { get; set; }
    [FromForm]
    public int IntegerFromForm { get; set; }
    public int IntegerNotUsed { get; set; }
    [FromQuery]
    public float FloatFromQuery { get; set; }
    [FromForm]
    public float FloatFromForm { get; set; }
    public float FloatNotUsed { get; set; }
    [FromQuery]
    public bool BooleanFromQuery { get; set; }
    [FromForm]
    public bool BooleanFromForm { get; set; }
    public bool BooleanNotUsed { get; set; }
    
    [FromHeader(Name = "x-string")]
    public string StringHeader { get; set; } = null!;

    [FromHeader(Name = "x-number")] public int NumberHeader { get; set; } = 5;
    
    [FromHeader(Name = "x-nullable-number")]
    public int? NullableHeader { get; set; }
}

snippet source | anchor

And the corresponding test case for utilizing this:

cs
var result = await Host.Scenario(x => x
    .Post
    .FormData(new Dictionary<string, string>
    {
        { "EnumFromForm", "east" },
        { "StringFromForm", "string2" },
        { "IntegerFromForm", "2" },
        { "FloatFromForm", "2.2" },
        { "BooleanFromForm", "true" },
        { "StringNotUsed", "string3" }
    }).QueryString("EnumFromQuery", "west")
    .QueryString("StringFromQuery", "string1")
    .QueryString("IntegerFromQuery", "1")
    .QueryString("FloatFromQuery", "1.1")
    .QueryString("BooleanFromQuery", "true")
    .QueryString("IntegerNotUsed", "3")
    .ToUrl("/api/asparameters1")
);
var response = await result.ReadAsJsonAsync<AsParametersQuery>();
response.EnumFromForm.ShouldBe(Direction.East);
response.StringFromForm.ShouldBe("string2");
response.IntegerFromForm.ShouldBe(2);
response.FloatFromForm.ShouldBe(2.2f);
response.BooleanFromForm.ShouldBeTrue();
response.EnumFromQuery.ShouldBe(Direction.West);
response.StringFromQuery.ShouldBe("string1");
response.IntegerFromQuery.ShouldBe(1);
response.FloatFromQuery.ShouldBe(1.1f);
response.BooleanFromQuery.ShouldBeTrue();
response.EnumNotUsed.ShouldBe(default);
response.StringNotUsed.ShouldBe(default);
response.IntegerNotUsed.ShouldBe(default);
response.FloatNotUsed.ShouldBe(default);
response.BooleanNotUsed.ShouldBe(default);

snippet source | anchor

Wolverine.HTTP is also able to support [FromServices], [FromBody], and [FromRoute] bindings as well as shown in this sample from the tests:

cs
public class AsParameterBody
{
    public string Name { get; set; } = null!;
    public Direction Direction { get; set; }
    public int Distance { get; set; }
}

public class AsParametersQuery2
{
    // We do a check inside of an HTTP endpoint that this works correctly
    [FromServices, JsonIgnore]
    public IDocumentStore Store { get; set; } = null!;

    [FromBody]
    public AsParameterBody Body { get; set; } = null!;

    [FromRoute]
    public string Id { get; set; } = null!;
    
    [FromRoute]
    public int Number { get; set; }
}

public static class AsParametersEndpoints2{
    [WolverinePost("/asp2/{id}/{number}")]
    public static AsParametersQuery2 Post([AsParameters] AsParametersQuery2 query)
    {
        // Just proving the service binding works
        query.Store.ShouldBeOfType<DocumentStore>();
        return query;
    }
}

snippet source | anchor

Splitting a Route Value from the Request Body

A common shape is an endpoint whose route carries the identity (say, an aggregate id) while the JSON body carries the rest of the command. If you bind the body to a single plain type that also has a property matching the route token, that property is duplicated — it shows up both as a route parameter and as a property of the request body in the generated OpenAPI:

cs
// {journeyId} is in the route AND JourneyId is a property of the body type,
// so journeyId appears twice in the OpenAPI document (in: path and in the body schema)
public record AddPassengerCommand(Guid JourneyId, string PassengerName);

[WolverinePost("/journey/{journeyId:guid}/passenger")]
public static string Post(AddPassengerCommand command) => "ok";

This is the same behavior as ASP.NET Core's own minimal APIs — neither framework strips a body property just because its name happens to match a route token. The idiomatic way to avoid the duplication is to bind with [AsParameters] and explicitly source the id from the route and the rest from the body:

cs
public record AddPassengerCommand(
    [FromRoute] Guid JourneyId,
    [FromBody] AddPassengerCommand.Payload Body)
{
    public record Payload(string PassengerName);
}

[WolverinePost("/journey/{journeyId:guid}/passenger")]
public static string Post([AsParameters] AddPassengerCommand command)
    => $"{command.JourneyId}: {command.Body.PassengerName}";

Now journeyId is described only as a route parameter (with its real type/format, e.g. uuid), and the request body schema is just the Payload type — the route id is not duplicated in the body. This works the same way when the route id also feeds a Marten/Polecat [WriteAggregate]/[Aggregate] parameter on the same endpoint.

Using Records

And lastly, you can use C# records or really just any constructor function as well and decorate parameters like so:

cs
public record AsParameterRecord(
    [FromRoute] string Id,
    [FromQuery] int Number,
    [FromHeader(Name = "x-direction")] Direction Direction,
    [FromForm(Name = "test")] bool IsTrue);

public static class AsParameterRecordEndpoint
{
    [WolverinePost("/asparameterrecord/{Id}")]
    public static AsParameterRecord Post([AsParameters] AsParameterRecord input) => input;
}

snippet source | anchor

Strict Query String Binding 6.18

By default in Wolverine 6.x, a query string value that is present but cannot be parsed to the target member type (an enum, int, Guid, etc.) is silently ignored — the member simply keeps its default or property initializer value. ASP.NET Core's own minimal API [AsParameters] binder returns 400 Bad Request for the same input, and the lenient behavior makes a typo'd query value indistinguishable from an absent one (not even FluentValidation can catch it downstream, because the bound value looks legitimate).

You can opt into the strict, minimal-API-compatible behavior with WolverineHttpOptions.RejectUnparseableQueryValues:

cs
app.MapWolverineEndpoints(opts =>
{
    // Return 400 with a ProblemDetails body naming the offending parameter
    // when a query string value is present but unparseable
    opts.RejectUnparseableQueryValues = true;
});

The behavior matrix for a bound query string member (for example [FromQuery] public SortField SortBy { get; set; } = SortField.Date; on GET /search):

RequestFlag off (default)Flag on
GET /search?SortBy=Name200 — binds Name200 — binds Name
GET /search (missing)200 — keeps the initializer (Date)200 — keeps the initializer (Date)
GET /search?SortBy=bogus (malformed)200 — keeps the initializer (Date)400 — ProblemDetails naming SortBy

Only a present but unparseable value triggers the 400; a missing query string value keeps the member's default / initializer in both modes. The flag applies to query string binding for [AsParameters] members and for endpoint method arguments bound from the query string alike. Route argument binding is unaffected (an unparseable route value already returns 404).

Collections 6.18

The flag covers collection query string parameters (T[], List<T>, IList<T>, IReadOnlyList<T>, IEnumerable<T>) too. With the flag off, an element that fails to parse is silently dropped, which is especially dangerous for an optional filter: the endpoint sees null (or a partial collection) and quietly returns an unfiltered 200 while the caller believes the results were filtered.

With the flag on, binding a collection is all or nothing — a single unparseable element rejects the whole request with a 400 naming the parameter, rather than silently dropping the bad element and keeping the good ones. For [FromQuery] public Colour[]? Colours { get; set; } on GET /widgets:

RequestFlag off (default)Flag on
GET /widgets?Colours=Red&Colours=Blue200 — binds [Red, Blue]200 — binds [Red, Blue]
GET /widgets (missing)200 — keeps the initializer (null)200 — keeps the initializer (null)
GET /widgets?Colours=Purple200Colours is null400 — ProblemDetails naming Colours
GET /widgets?Colours=Red&Colours=Purple200 — binds only [Red]400 — ProblemDetails naming Colours

String collections are unaffected in both modes, as there is nothing to parse.

WARNING

RejectUnparseableQueryValues is opt-in (defaults to false) throughout Wolverine 6.x to preserve the previous lenient behavior, but the default flips to true (strict) in Wolverine 7.0. If you depend on the lenient behavior, set the flag explicitly to be ready for 7.0.

The Fluent Validation middleware for Wolverine.HTTP is able to validate against request types bound with [AsParameters]:

cs
public static class ValidatedAsParametersEndpoint
{
    [WolverineGet("/asparameters/validated")]
    public static string Get([AsParameters] ValidatedQuery query)
    {
        return $"{query.Name} is {query.Age}";
    }
}

public class ValidatedQuery
{
    [FromQuery]
    public string? Name { get; set; }
    
    public int Age { get; set; }

    public class ValidatedQueryValidator : AbstractValidator<ValidatedQuery>
    {
        public ValidatedQueryValidator()
        {
            RuleFor(x => x.Name).NotNull();
        }
    }
}

snippet source | anchor

Released under the MIT License.