I recently worked on a proof of concept library that involves access to the web request at the HttpContext level. The library I’m building needs to register endpoints, and these endpoints need to process basic query string parameters. Fundamental stuff, but fundamentals don’t have to be tedious and difficult.

In this post, I’ll show you a set of extension methods that can help us reduce the noise levels in our endpoint registration, and give us a head start in converting our parameters to our target types.

The Querystring And StringValues

Let’s take a look at our HTTP URL to understand what we’ll be parsing.

https://example.com?skip=0&take=100

We have a query string with two parameters of skip and take. When we investigate the HttpContext.Request.Query property, we notice that the struct StringValues comprises the whole of QueryCollection. Before continuing, let take a look at the struct.

The summary on the struct describes StringValues as a construct that “Represents zero/null, one, or many strings in an efficient way.” The definition of this struct implements a buffet of collection interfaces.

public readonly struct StringValues : 
  ICollection<string>, 
  IEnumerable<string>, 
  IEnumerable, IList<string>, 
  IReadOnlyCollection<string>, 
  IReadOnlyList<string>, 
  IEquatable<StringValues>,
  IEquatable<string>,
  IEquatable<string[]>
{
}

Why is ASP.NET using this structure? Well, it’s easier to explain by modifying our original query string.

https://example.com?skip=0&take=100&skip=10

Notice that we see a value for skip appear multiple times. If we weren’t using StringValues then one of those values would be lost during the serialization process.

Making It Easier

You may have noticed in the collection of interfaces implemented by StringValues that all of the generic parameters are string. We can represent all query string parameters as string, but its not ideal. Let’s look at an example of accessing the values from the example query.

int skip = 0;
if (ctx.Request.Query.TryGetValue("skip", out var skips))
{
    var first = skips.FirstOrDefault() ?? string.Empty;
    if (int.TryParse(first, out var value))
    {
        skip = value;
    }
}

We first need to use TryGetValue to determine if our parameter exists in the query string. We then need to follow up the call with a TryParse method. We need to repeat all the steps for each new query string parameter.

Luckily, I’ve created a set of extension methods that make accessing values from a QueryCollection much more enjoyable.

int skip, take;
skip = ctx.Request.Query.Get<int>(nameof(skip));
take = ctx.Request.Query.Get(nameof(take), @default: 100);
take = ctx.Request.Query.Get(nameof(take), 100, ParameterPick.Last);

The implementation shown here will get the first value in our query string. As you’ll see, we can also change the behavior by passing in a ParameterPicker enum that switches between First and Last. If we need to get all potential parameters that convert successfully, we can use an All method.

var takes = ctx.Request
    .Query
    .All<int>("take");

Let’s take a look at the implementation.

public static class IQueryCollectionExtensions
{
    public static IEnumerable<T> All<T>(
        this IQueryCollection collection, 
        string key)
    {
        var values = new List<T>();
        
        if (collection.TryGetValue(key, out var results))
        {
            foreach (var s in results)
            {
                try
                {
                    var result = (T) Convert.ChangeType(s, typeof(T));
                    values.Add(result);
                }
                catch (Exception)
                {
                    // conversion failed
                    // skip value
                }
            }
        }
        
        // return an array with at least one
        return values;
    }
    
    public static T Get<T>(
        this IQueryCollection collection,
        string key,
        T @default = default,
        ParameterPick option = ParameterPick.First)
    {
        var values = All<T>(collection, key);
        var value = @default;
        
        if (values.Any())
        {
            value = option switch
            {
                ParameterPick.First => values.FirstOrDefault(),
                ParameterPick.Last => values.LastOrDefault(),
                _ => value
            };
        }

        return value ?? @default;
    }
}

public enum ParameterPick
{
    First,
    Last
}

The real magic happens when we use the Convert.ChangeType method. As long as we are dealing with simple primitive types, we should be able to convert from a string to something like an int, double, or bool. This extension removes a lot of repetitiveness from our original codebase and boosts the signal to noise ratio in our endpoint registrations.

Conclusion

Extension methods are awesome and using them to enhance a tedious API can make a world of difference. In this case, we can use these extension methods to access StringValues in a way that works for the logic in our code, while respecting the edge case of multiple values. We’ve also given ourselves the opportunity to alter behavior via the ParameterPick enum. Finally, the All method can be used if we need all convertable values.

Hope you found this post useful, and please leave a comment.