Parsing Query Strings in .NET Core
We recently needed to parse and modify some query strings while building Request Metrics. Query string parsing has never been pleasant in .NET, has it improved in .NET Core?
We were familiar with HttpUtility.ParseQueryString()
for the task, but that API has a major landmine. With the release of .NET Core, Microsoft took another swing at it. We figured we’d try the new way and see how they did! If you want the fully uncensored version, check out the video above.
The Old Way
Back in the “old” days when you had to parse a query string and modify it, you’d almost always turn to HttpUtility.ParseQueryString
. This utility lives in System.Web
and, if the Microsoft documentation is to be believed, returns a nice NameValueCollection
of query string param name/values.
But the big secret is this method doesn’t return a NameValueCollection
. No, it returns a special subclass called HttpValueCollection
! And the HttpValueCollection
has some neat tricks up its sleeve. One of those tricks is that you can call .ToString()
on it to retrieve the full url encoded query string! It also supports multiple values for the same param name using regular ICollection.Add()
semantics.
The problem with this API is that you have to know you’re dealing with a special HttpValueCollection
that can url encode, handle multiple params etc. It’s confusing and not well documented, but once you know the trick this is a pretty slick API to use for most tasks.
The New Way
Enter .NET Core. Microsoft had a chance to redo the query string parsing APIs. Unfortunately instead of simplifying they made it more complicated.
Instead of a single class (albeit with magic functionality), we now have three that play a role. The QueryHelpers
class parses query strings. The QueryString
struct represents an entire query string (and prints the URL encoded version). And the StringValues
struct represents parameter values (zero/null, one, or many).
Let’s use these new APIs to do the same things we did with HttpUtility.ParseQueryString
Oh no! When we try and add the second value for the same param name, we get an error. That’s because there’s no magic helper functionality this time. The parsed query string is really just a Dictionary<String, StringValues>
- and you can’t add the same key more than once to a dictionary! So what do we do? How do we add a second param of the same name?
So, we can do the same things we did before, but there’s a lot more friction. The StringValues
struct models the behavior of multiple query string parameters with the same name correctly, but it sacrifices developer ergonomics. Concat
is a nice static helper, but why can’t I just .Add()
to it? And it gets more painful if I have two values for the same parameter name and want to add a third.
And what of the QueryString
class? It can parse a string
in to a QueryString
, but it lacks the basic remove/update functionality you’d expect! You can’t use it to edit or remove a parameter.
Conversely, the QueryHelpers
can return a Dictionary<String, StringValues>
after parsing, but it can’t URL encode or otherwise assist with producing a usable URI query string. So you end up having to use all three to manage it.
I confess I don’t understand Microsoft’s reasoning here. To improve the API all they really had to do was document the original one better. Maybe adjust some of the behavior and put it in a new namespace to avoid backwards compatibility confusion. Instead they developed a whole new set of APIs. Ones that are more cumbersome to work with.
In any case, you now have two options when working in .NET Core. You can use the old way or the new way depending on your preference. Me personally, I’m sticking with the old way :)
If you’re still reading, check out more of our story building Request Metrics!