Web Development

Using Query String Parameters with Minimal APIs

In this article, we are going to explain how we can work with query string parameters in Minimal APIs in .NET 6. You may refer here for a nice introduction to the concept of Minimal APIs. Also, we will show new improvements that .NET 7 brings to the table.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Handling Simple Query String Parameters With Minimal APIs

In our examples, we will work again with the idea of a blog, where we can add, edit, remove, and read articles. Also, we need to add search functionality to this blog. We want to be able to filter articles based on the author and the year of the article’s publication:

app.MapGet("/search", (string author, int yearPublished) =>
{
    return $"Author: {author}, Year published: {yearPublished}";
});

When we call /search?author=John Doe&yearPublished=2022, we see that the query string parameters are automatically mapped to the lambda method parameters.

Note that we cannot use default values in the lambda method definition. In this case, we need to pass a method as a delegate:

app.MapGet("/search", ArticleMapping);

string ArticleMapping(string author = "N/A", int yearPublished = 0) 
{
    return $"Author: {author}, Year published: {yearPublished}";
}

Handling Complex Query String Parameters With Minimal APIs

Now, let’s try to map the query parameters into a class named SearchCriteria:

app.MapGet("/search", ([FromQuery] SearchCriteria criteria) =>
{
    return $"Author: {criteria.Author}, Year published: {criteria.YearPublished}";
});

public class SearchCriteria
{
    public string? Author { get; set; }
    public int YearPublished { get; set; }
}

It seems that we cannot bind complex objects from query string parameters, as we get an Exception: System.InvalidOperationException: No public static bool SearchCriteria.TryParse(string, out SearchCriteria) method found for criteria.  

Fortunately, we can make this mapping ourselves. There are two possible options; both involve the creation of a static method inside the complex object definition. Those methods are:

  • BindAsync
  • TryParse

BindAsync is the solution to our problem, so we will check it first. TryParse is more appropriate for binding simple string values and we will explore it later, with a slightly different example.

BindAsync

We should define BindAsync as a static function inside the SearchCriteria class. Then, we can use the HttpContext object to get the query string parameters and map them to the appropriate object properties:

app.MapGet("/search", (SearchCriteria criteria) =>
{
    return $"Author: {criteria.Author}, Year published: {criteria.YearPublished}";
});

public class SearchCriteria
{
    public string? Author { get; set; }
    public int YearPublished { get; set; }

    public static ValueTask<SearchCriteria?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        string author = context.Request.Query["Author"];
        int.TryParse(context.Request.Query["YearPublished"], out var year);

        var result = new SearchCriteria
        {
            Author = author,
            YearPublished = year
        };

        return ValueTask.FromResult<SearchCriteria?>(result);
    }
}

When we call /search?author=John Doe&yearPublished=2022 we get a SearchCriteria object containing the query string parameters.

If there is a problem with binding, e.g. the publication year is not correct, then we have two options:

  • Return null from BindAsync: This will result in returning HTTP 400 Bad Request to the client.
  • Throw an exception: An HTTP 500 Internal Server Error response will be returned.

TryParse

As we already mentioned, TryParse is more appropriate for binding simple string values. Suppose we want to provide users with the option to search for multiple article IDs: /search?ids=3,5,12,14

Of course, we can map this query parameter as a simple string and then split and parse it:

app.MapGet("/search", (string ids) =>
{
    var idList = new List<int>();
    var trimmedValue = ids?.TrimStart('(').TrimEnd(')');
    var segments = trimmedValue?.Split(',',
            StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

    foreach (var segment in segments)
    {
        int.TryParse(segment, out var id);
        idList.Add(id);
    }
});

But what if we would like to have them automatically mapped into a list? Well, not actually a list, but an object that contains a list. In this case, we can also use TryParse:

app.MapGet("/search", (ArticleIDs ids) =>
{
    var text = new StringBuilder();
    foreach(var id in ids.IDs)
    {
        text.Append(id + " ");
    }
    return $"IDs: {text.ToString()}";
});

public class ArticleIDs
{
    public List<int> IDs = new List<int>();

    public static bool TryParse(string? value, IFormatProvider? provider, out ArticleIDs? articleIDs)
    {
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

        if (segments == null)
        {
            articleIDs = new ArticleIDs();
            return false;
        }

        var idList = new List<int>();
        foreach (var segment in segments)
        {
            int.TryParse(segment, out int id);
            idList.Add(id);
        }

        articleIDs = new ArticleIDs()
        {
            IDs = idList
        };

        return true;
    }
}

When we call /search?ids=3,5,12,14 , the value parameter contains the string with the article IDs. If there is a problem parsing the query string (e.g. no IDs were passed) we can return a false from TryParse; as a result, an HTTP 400 Bad Request response will be returned to the client.

Using Query String Parameters With Minimal APIs In .NET 7

For our previous examples, we used .NET 6, the current .NET version at the time of writing. In .NET 7 there are until now two new exciting developments.

First of all, we can bind arrays and StringValues from query strings:

// GET  /search?id=1&id=4&id=7
app.MapGet("/search", (int[] ids) =>
          $"Article IDs: {ids[0]}, {ids[1]}, {ids[2]}");

Moreover, we can use the [AsParameters] attribute to automatically map the query parameters into an object, without the need for custom mapping with BindAsync or TryParse:

app.MapGet("/search", ([AsParameters] SearchCriteria criteria) =>
{
    return $"Author: {criteria.Author}, Year published: {criteria.YearPublished}";
});

Conclusion

In this article, we have learned how to deal with query string parameters in Minimal APIs in .NET 6. We have also got an idea of the improvements that have been introduced in  .NET 7.

Code Maze

Share
Published by
Code Maze

Recent Posts

Code Maze Weekly #149

Issue #149 of the Code Maze weekly. Check out what's new this week and enjoy…

Updated Date Nov 25, 2022

C# String Interpolation

Very early in the history of programming, we've seen the need to use text on…

Updated Date Nov 24, 2022

How to Check if a String Ends With a Number in C#

Checking if a string ends with a number in C# is a very common operation.…

Updated Date Nov 25, 2022

How to Execute Stored Procedures With EF Core 7

In this article, we will see how to use stored procedures in Entity Framework Core…

Updated Date Nov 22, 2022

HashSet vs SortedSet in C#

The HashSet<T> and SortedSet<T> classes in the System.Collections.Generic namespace define two ways of storing and iterating…

Updated Date Nov 22, 2022

Code Maze Weekly #148

Issue #148 of the Code Maze weekly. Check out what's new this week and enjoy…

Updated Date Nov 18, 2022