C# – Sending query strings with HttpClient

Query strings start with ‘?’ and have one or more key-value pairs separated by ‘&’. All characters except a-z, A-Z, 0-9 have to be encoded, including Unicode characters.

When you use HttpClient, it automatically encodes the URI for you (internally, it delegates this task to the Uri class). This means when you include a query string in the URI, you don’t need to encode it yourself.

Here’s an example of sending a request with a query string:

await httpClient.GetAsync("https://localhost:12345/movies/search?name=John Wick&hasWords=Ελληνικά");
Code language: C# (cs)

Here is the actual request:

GET https://localhost:12345/movies/search?name=John%20Wick&hasWords=%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%B9%CE%BA%CE%AC HTTP/1.1Code language: plaintext (plaintext)

It encoded ‘John Wick’ as ‘John%20Wick’, and Ελληνικά, which is composed of Unicode characters in the Greek alphabet, as ‘%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%B9%CE%BA%CE%AC’.

You can either hardcode the query string as shown above, or build it. The simplest way to build the query string is to use QueryHelpers.AddQueryString() (from Microsoft.AspNetCore.WebUtilities). I’ll show how to use that below, and how to create your own query string builder.

Building a query string with QueryHelpers.AddQueryString()

Here’s an example of using QueryHelpers to build a query string:

using Microsoft.AspNetCore.WebUtilities;

var query = new Dictionary<string, string>()
{
	["name"] = "Dune",
	["year"] = "2021"
};

var uri = QueryHelpers.AddQueryString("https://localhost:12345/movies/search", query);

var result = await httpClient.GetAsync(uri);
Code language: C# (cs)

Here’s the actual request:

GET https://localhost:12345/movies/search?name=Dune&year=2021 HTTP/1.1Code language: plaintext (plaintext)

Get Microsoft.AspNetCore.WebUtilities

QueryHelpers is in the Microsoft.AspNetCore.WebUtilities package. If you’re not working on an ASP.NET Core project, you can add the package (View > Other Windows > Package Manager Console):

Install-Package Microsoft.AspNetCore.WebUtilities
Code language: PowerShell (powershell)

This is available for .NET Standard 2.0, which means it works in: .NET Core 1.0+, .NET Framework 4.6.1+, .NET 5+, Unity 2018.1+, and more.

What happens when you pass an encoded URI to the HttpClient?

QueryHelpers.AddQueryString() formats and encodes the query string. Since HttpClient already encodes the URI, you may be wondering what happens if you pass it an encoded query string? Don’t worry, it won’t double-encode it.

Here’s an example of passing in an encoded query string:

await httpClient.GetAsync("https://localhost:12345/movies/search?name=John%20Wick&hasWords=%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%B9%CE%BA%CE%AC")
Code language: C# (cs)

It will send this as-is, since the URI is already encoded. Here is the actual request:

GET https://localhost:12345/movies/search?name=John%20Wick&hasWords=%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%B9%CE%BA%CE%AC HTTP/1.1Code language: plaintext (plaintext)

Using your own query string builder

Let’s say you don’t want to add the Microsoft.AspNetCore.WebUtilities package, or perhaps you want to customize how the query strings are built. In that case, you can use your own query string builder, using the QueryHelper.AddQueryString() source code as a starting point.

Here’s a stripped down version with encoding removed:

public static class RequestUriUtil
{
	public static string GetUriWithQueryString(string requestUri, 
		Dictionary<string, string> queryStringParams)
	{
		bool startingQuestionMarkAdded = false;
		var sb = new StringBuilder();
		sb.Append(requestUri);
		foreach (var parameter in queryStringParams)
		{
			if (parameter.Value == null)
			{
				continue;
			}

			sb.Append(startingQuestionMarkAdded ? '&' : '?');
			sb.Append(parameter.Key);
			sb.Append('=');
			sb.Append(parameter.Value);
			startingQuestionMarkAdded = true;
		}
		return sb.ToString();
	}
}
Code language: C# (cs)

QueryHelpers encodes the keys/values (ex: UrlEncoder.Default.Encode(parameter.Key)), whereas this code is only doing the formatting. Remember that HttpClient will automatically encode it, so it’s not necessary to encode here.

Here’s an example of using this query string builder:

var query = new Dictionary<string, string>()
{
	["name"] = "John Wick",
	["year"] = "2014",
	["hasWords"] = "Ελληνικά"
};

var requestUriWithQuery = RequestUriUtil.GetUriWithQueryString("https://localhost:12345/movies/search", query);

var result = await httpClient.GetAsync(requestUriWithQuery);
Code language: C# (cs)

Here’s the actual request:

GET https://localhost:12345/movies/search?name=John%20Wick&year=2014&hasWords=%CE%95%CE%BB%CE%BB%CE%B7%CE%BD%CE%B9%CE%BA%CE%AC HTTP/1.1Code language: plaintext (plaintext)

HttpClient GetWithQueryStringAsync() extension method

If you’re using HttpClient and a query string builder, you may want to use an extension method to simplify the calling code (and potentially remove duplication):

using Microsoft.AspNetCore.WebUtilities;

public static class HttpClientExtensions
{
	public static async Task<HttpResponseMessage> GetWithQueryStringAsync(this HttpClient client, string uri, 
		Dictionary<string, string> queryStringParams)
	{
		var url = QueryHelpers.AddQueryString(uri, queryStringParams);

		return await client.GetAsync(url);
	}
}
Code language: C# (cs)

Use it like this:

var query = new Dictionary<string, string>()
{
	["name"] = "Dune",
	["year"] = "2021"
};

var result = await httpClient.GetWithQueryStringAsync("https://localhost:12345/movies/search", query);
Code language: C# (cs)

HttpClient has lots of methods with many overloads, so you can use this a starting point to add wrappers for whatever methods you’re using.

This is especially useful if you’re using your own query string builder, because you can make it a private method that’s only used by the HttpClient extension methods:

public static class HttpClientExtensions
{
	public static async Task<HttpResponseMessage> GetWithQueryStringAsync(this HttpClient client, string uri,
		Dictionary<string, string> queryStringParams)
	{
		var url = GetUriWithQueryString(uri, queryStringParams);

		return await client.GetAsync(url);
	}

	private static string GetUriWithQueryString(string requestUri,
			Dictionary<string, string> queryStringParams)
	{
		bool startingQuestionMarkAdded = false;
		var sb = new StringBuilder();
		sb.Append(requestUri);
		foreach (var parameter in queryStringParams)
		{
			if (parameter.Value == null)
			{
				continue;
			}

			sb.Append(startingQuestionMarkAdded ? '&' : '?');
			sb.Append(parameter.Key);
			sb.Append('=');
			sb.Append(parameter.Value);
			startingQuestionMarkAdded = true;
		}
		return sb.ToString();
	}
}
Code language: C# (cs)