Blog Home » Parsing Query Strings in .NET Core

Parsing Query Strings in .NET Core

By Eric Brandes

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.

  var queryString = HttpUtility.ParseQueryString("?param1=value");
  // queryString.GetType().Name --> HttpValueCollection!

  // Adding a parameter
  queryString.Add("param2", "my value");

  // Adding a second parameter with the same name
  queryString.Add("param2", "my other value");

  // Getting a param value (multiple values are comma separated)
  var param2Value = queryString["param2"];
  // --> "my value,my other value"

  // Modifying a parameter
  queryString["param1"] = "another value";

  // Url Encoding the whole thing
  // --> "param1=another+value&param2=my+value&param2=my+other+value

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

 var queryString = QueryHelpers.ParseQuery("?param1=value");
  // queryString.GetType() --> typeof(Dictionary<String,StringValues>)

  // Adding a parameter
  queryString.Add("param2", "my value"); // Easy so far.

  // Adding a second parameter with the same name
  queryString.Add("param2", "my other value"); // ArgumentException!
  // --> An item with the same key has already been added

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?

  // Adding a second parameter with the same name (round 2).
  StringValues twoValues = StringValues.Concat(queryString["param2"], "my other value");
  parsedQueryString["coronavirus"] = twoValues;

  // Getting a param value
  var param2Value = queryString["param2"];
  // --> StringValues!

  param2Value.ToString(); // Get the values concatenated together
  param2Value.ToArray(); // Gets an array of strings

  // Modifying a parameter
  queryString["param1"] = "another value";
  // NOTE, if there were two values,
  // this overwrites both and leaves a single value.

  // Url Encoding the whole thing
  // --> "?param1=another%20value&param2=my%20value&param2=my%20other%20value

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!

Eric Brandes
Eric Brandes
CTO and Cofounder