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.