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.

Working with QueryString ​

WARNING

WolverineHttpOptions.RejectUnparseableQueryValues defaults to false, and this is the most surprising default in Wolverine.HTTP if you are arriving from MVC or minimal APIs, both of which reject an unparseable value.

Under the default, a query string value that is present but unparseable binds the parameter's default and the request proceeds:

RequestParameterResult
?page=abcint pageruns with page = 0
?since=yesterdayDateTimeOffset sinceruns with default
?ids=1,x,3int[] idsruns with the bad element dropped

Nothing is logged for the individual request, so the server sees what looks like a perfectly valid call — which is what makes "the API returned the wrong page" so hard to chase. Wolverine logs one warning at startup when an application binds parsed query parameters and the flag is off.

Set it to true to get a 400 Bad Request with a ProblemDetails body naming the offending parameter instead:

csharp
app.MapWolverineEndpoints(opts =>
{
    opts.RejectUnparseableQueryValues = true;
});

A missing query string value still binds the parameter's default in either mode; this governs only values that are present and cannot be parsed. See Strict Query String Binding.

The strict behavior becomes the default in Wolverine 7.0.

TIP

Wolverine can handle both nullable types and the primitive values here, so int and int? are both valid.

Wolverine supports passing query string values to your HTTP method arguments for the exact same set of value types supported for route arguments. In this case, Wolverine treats any value type parameter where the parameter name does not match a route argument name as coming from the HTTP query string.

When Wolverine does the runtime matching, it's using the exact parameter name as the query string key. Here's a quick sample:

cs
[WolverineGet("/querystring/string")]
public static string UsingQueryString(string name) // name is from the query string
{
    return name.IsEmpty() ? "Name is missing" : $"Name is {name}";
}

snippet source | anchor

And the corresponding tests:

cs
[Fact]
public async Task use_string_querystring_hit()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/string?name=Magic");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    var text = await body.ReadAsTextAsync();
    text.ShouldBe("Name is Magic");
}

[Fact]
public async Task use_string_querystring_miss()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/string");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    var text = await body.ReadAsTextAsync();
    text.ShouldBe("Name is missing");
}

[Fact]
public async Task use_decimal_querystring_hit()
{
    var body = await Scenario(x =>
    {
        x.WithRequestHeader("Accept-Language", "fr-FR");
        x.Get.Url("/querystring/decimal?amount=42.1");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    var text = await body.ReadAsTextAsync();
    text.ShouldBe("Amount is 42.1");
}

[Fact]
public async Task use_decimal_fromquery_hit()
{
    // GH-3586 follow-up: an explicit [FromQuery] on a scalar decimal used to throw at endpoint
    // discovery ("System.Decimal has multiple constructors"). It now binds from its own key.
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/decimal2?value=42.1");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("42.1");
}

[Fact]
public async Task use_nullable_decimal_fromquery_hit()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/decimal2/nullable?value=42.1");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("42.1");
}

[Fact]
public async Task use_nullable_decimal_fromquery_miss()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/decimal2/nullable");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("Value is missing");
}

[Fact]
public async Task use_enum_fromquery_hit_with_string_name()
{
    // Enum values arrive as their string name on the wire, not as an integer.
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/enum2?value=North");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("North");
}

[Fact]
public async Task use_enum_fromquery_hit_with_lowercase_string_name()
{
    // ...and the name parse is case-insensitive.
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/enum2?value=west");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("West");
}

[Fact]
public async Task use_nullable_enum_fromquery_hit_with_string_name()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/enum2/nullable?value=South");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("South");
}

[Fact]
public async Task use_nullable_enum_fromquery_miss()
{
    var body = await Scenario(x =>
    {
        x.Get.Url("/querystring/enum2/nullable");
        x.Header("content-type").SingleValueShouldEqual("text/plain");
    });

    (await body.ReadAsTextAsync()).ShouldBe("Value is missing");
}

snippet source | anchor

[FromQuery] Binding 3.12 ​

Wolverine can support the FromQueryAttribute binding similar to MVC Core or Minimal API. Let's say that you have a GET endpoint where you may want to use a series of non-mandatory querystring values for a query, and it would be convenient to have Wolverine just let you declare a single .NET type for all the optional query string values that will be filled with any of the matching query string parameters like this sample:

cs
// If you want every value to be optional, use public, settable
// properties and a no-arg public constructor
public class OrderQuery
{
    public int PageSize { get; set; } = 10;
    public int PageNumber { get; set; } = 1;
    public bool? HasShipped { get; set; }
}

// Or -- and I'm not sure how useful this really is, use a record:
public record OrderQueryAlternative(int PageSize, int PageNumber, bool HasShipped);

public static class QueryOrdersEndpoint
{
    [WolverineGet("/api/orders/query")]
    public static Task<IPagedList<Order>> Query(
        // This will be bound from query string values in the HTTP request
        [FromQuery] OrderQuery query, 
        IQuerySession session,
        CancellationToken token)
    {
        IQueryable<Order> queryable = session.Query<Order>()
            // Just to make the paging deterministic
            .OrderBy(x => x.Id);

        if (query.HasShipped.HasValue)
        {
            queryable = query.HasShipped.Value 
                ? queryable.Where(x => x.Shipped.HasValue) 
                : queryable.Where(x => !x.Shipped.HasValue);
        }

        // Marten specific Linq helper
        return queryable.ToPagedListAsync(query.PageNumber, query.PageSize, token);
    }
}

snippet source | anchor

Because we've used the [FromQuery] attribute on a parameter argument that's not a simple type, Wolverine is trying to bind the query string values to each public property of the OrderQuery object being passed in as an argument to QueryOrdersEndpoint.Query().

Here's the code that Wolverine generates around the method signature above (warning, it's ugly code):

csharp
// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime;

namespace Internal.Generated.WolverineHandlers
{
    // START: GET_api_orders_query
    public class GET_api_orders_query : Wolverine.Http.HttpHandler
    {
        private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
        private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;
        private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;

        public GET_api_orders_query(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions)
        {
            _wolverineHttpOptions = wolverineHttpOptions;
            _wolverineRuntime = wolverineRuntime;
            _outboxedSessionFactory = outboxedSessionFactory;
        }



        public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
        {
            var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
            // Building the Marten session
            await using var querySession = _outboxedSessionFactory.QuerySession(messageContext);
            // Binding QueryString values to the argument marked with [FromQuery]
            var orderQuery = new WolverineWebApi.Marten.OrderQuery();
            if (int.TryParse(httpContext.Request.Query["PageSize"], System.Globalization.CultureInfo.InvariantCulture, out var PageSize)) orderQuery.PageSize = PageSize;
            if (int.TryParse(httpContext.Request.Query["PageNumber"], System.Globalization.CultureInfo.InvariantCulture, out var PageNumber)) orderQuery.PageNumber = PageNumber;
            if (bool.TryParse(httpContext.Request.Query["HasShipped"], out var HasShipped)) orderQuery.HasShipped = HasShipped;
            
            // The actual HTTP request handler execution
            var pagedList_response = await WolverineWebApi.Marten.QueryOrdersEndpoint.Query(orderQuery, querySession, httpContext.RequestAborted).ConfigureAwait(false);

            // Writing the response body to JSON because this was the first 'return variable' in the method signature
            await WriteJsonAsync(httpContext, pagedList_response);
        }

    }

    // END: GET_api_orders_query
    
    
}

Note there are some limitations of this approach in Wolverine:

  • Wolverine can use either a class that has a single constructor with arguments (like a record type) or a class with a public, default constructor and public settable properties but not have both a constructor with arguments and settable properties!
  • The types marked as [FromQuery] must be public, as well as any properties you want to bind
  • The binding supports array types, but know that you will always get an empty array as the value even with no matching query string values
  • Likewise, string values will be null if there is no query string
  • For any kind of parsed data (Guid, numbers, dates, boolean values, enums), Wolverine will not set any value on public setters if there is either no matching querystring value or the querystring value cannot be parsed

[AsParameters] Binding 3.13 ​

Also see the AsParameters binding.

Released under the MIT License.