Cover

How to Access Query Strings in Minimal APIs

I love that this job allows me to learn new stuff every day. In this case, I was building a simple API to use for some upcoming Pluralsight courses. I wanted to use Minimal APIs to expose some data for an old dataset from FiveThirtyEight on Bechdel Tests for Films. While I was adding paging, I got confused.

So, I started with a minimal API for getting all the films like so:

app.MapGet("api/films", async (BechdelDataService ds) =>
{

  if (ds is null) return Results.BadRequest();
  FilmResult data = await ds.LoadAllFilmsAsync();
  if (data.Results is null) return Results.NotFound();

  return Results.Ok(data);

}).WithTags("By Film").Produces(200).ProducesProblem(404);

Pretty simple Minimal API to get some data (my BechdelDataService is just a repository-like class). When I added paging, I wanted to use the typical Web API trick of using optional parameters:

app.MapGet("api/films", async (BechdelDataService ds, 
  int page = 1, 
  int pageSize = 50) =>
{
...
}).WithTags("By Film").Produces(200).ProducesProblem(404);

To my surprise this didn’t work. What I expected was that if I used a query string, it would bind to the extra parameters by name:

/api/films?page=1
/api/films

In this case, these should be the same. What is going on. At first I dove into seeing why Minimal APIs didn’t support this. I didn’t see much about it, I saw options to read the query strings manually:

app.MapGet("api/films", async (
    HttpRequest request, 
    BechdelDataService ds) =>
{
  var page = request.Query["page"] ?? 1;
  var size = request.Query["pageSize"] ?? 50;

...

    
}).WithTags("By Film").Produces(200).ProducesProblem(404);

This works, but I found it clunky. I wanted binding to work. I saw some examples that broke out the lambda to a method:

app.MapGet("api/films", GetAllFilms);

async Task<IResult> GetAllFilms(BechdelDataService ds, 
  int page = 1, 
  int pageSize = 50)
{
    ...
}

This works, so the problem wasn’t with Minimal APIs, the problem is that Lambdas do not support default values (since you’re passing in a lambda, the spec assumes that you’ll supply all values).

So this is a fix? I don’t like the messiness of breaking it out into separate methods. So how do we fix it? Let’s use nullable values:

app.MapGet("api/films", async (BechdelDataService ds, 
  int? page, 
  int? pageSize) =>
{
  if (ds is null) return Results.BadRequest();
  int pageNumber = page ?? 1;
  int pagerTake = pageSize ?? 50;

  FilmResult data = await ds.LoadAllFilmsAsync(pageNumber, pagerTake);
  if (data.Results is null) return Results.NotFound();

  return Results.Ok(data);

    
}).WithTags("By Film").Produces(200).ProducesProblem(404);

By supplying the values as nullable types (int?), we have to do our own defaulting, but I think it’s acceptable. One note here, is that I’m defining the types to non-nullable types to make my calls to the data service easier, but this isn’t necessary.

So, was I the only one who didn’t know that default values weren’t supported in lambda’s?