URL Encode in C#: Uri.EscapeDataString vs WebUtility.UrlEncode vs HttpUtility

.NET has at least four public methods that URL encode a string, and they do not agree. They differ in how they write spaces, whether hex digits are upper or lower case, and which punctuation they leave alone. Most of the time any of them "works", until a signature check, a cache key or a strict API compares the encoded text byte for byte. Below, each method is run on the same input on .NET 9, with a recommendation for each job.

The four encoders side by side

The test value contains an ampersand, a non-ASCII letter, a slash, a percent sign, a space, and the punctuation characters on which the methods disagree:

var v = "R&B café/50% off? (a+b)*!~'";

Uri.EscapeDataString(v);
// R%26B%20caf%C3%A9%2F50%25%20off%3F%20%28a%2Bb%29%2A%21~%27

WebUtility.UrlEncode(v);          // System.Net
// R%26B+caf%C3%A9%2F50%25+off%3F+(a%2Bb)*!%7E%27

HttpUtility.UrlEncode(v);         // System.Web
// R%26B+caf%c3%a9%2f50%25+off%3f+(a%2bb)*!%7e%27

Three differences stand out. EscapeDataString writes spaces as %20; the other two use +, the HTML form convention. EscapeDataString and WebUtility use upper-case hex, which RFC 3986 recommends, while HttpUtility uses lower case. And only EscapeDataString follows the RFC 3986 unreserved set exactly: it leaves A–Z a–z 0–9 - _ . ~ alone and encodes everything else, including ( ) * ! ', while the two form encoders keep ( ) * ! and encode ~.

All three encode text as UTF-8 first, so é becomes %C3%A9, and all three escape the ampersand. That is why they appear interchangeable in simple tests.

Which one to use

Use Uri.EscapeDataString by default. It is part of the base library on every .NET version, needs no extra assembly, matches what browsers, JavaScript's encodeURIComponent and most HTTP clients produce for spaces, and is safe for both query values and path segments.

Use WebUtility.UrlEncode when you specifically need application/x-www-form-urlencoded output, for example to imitate an HTML form post or to match a server that compares form-encoded strings. It lives in System.Net and is available everywhere, including .NET Standard.

Avoid HttpUtility.UrlEncode in new code unless you are maintaining ASP.NET (System.Web) code that already depends on it. Its lower-case hex is valid, but it is the odd one out, and mixing it with another encoder produces two different strings for the same value. That breaks OAuth 1.0 signatures and AWS Signature Version 4, both of which specify RFC 3986 encoding with upper-case hex.

Building a query string safely

The real goal is rarely "encode a string"; it is "build a URL with parameters". Encode each value on its own and let the framework join them. In ASP.NET Core, QueryHelpers.AddQueryString does both:

using Microsoft.AspNetCore.WebUtilities;

var url = QueryHelpers.AddQueryString("https://api.test/search",
    new Dictionary<string, string?> { ["q"] = "R&B café", ["page"] = "2" });
// https://api.test/search?q=R%26B%20caf%C3%A9&page=2

Outside ASP.NET Core, HttpUtility.ParseQueryString(string.Empty) returns a collection whose ToString() produces an encoded query, but with + and lower-case hex (q=R%26B+caf%c3%a9&page=2). Building the query from Uri.EscapeDataString pieces is the portable option:

var query = string.Join("&", parameters.Select(p =>
    $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));

var uri = new UriBuilder("https://api.test/search") { Query = query }.Uri;
uri.AbsoluteUri;   // https://api.test/search?q=R%26B%20caf%C3%A9

Never build the query by concatenating raw values and encoding the result at the end. Once the string is assembled, no encoder can tell which & separates parameters and which belongs to a value.

Encoding path segments

Path segments need %20 for spaces, because + is a literal plus in a path, and they need / encoded when it is part of a name rather than a separator. Uri.EscapeDataString does both:

var file = "Q3 report/final.pdf";
var url = $"https://files.test/docs/{Uri.EscapeDataString(file)}";
// https://files.test/docs/Q3%20report%2Ffinal.pdf

HttpUtility.UrlPathEncode("a b/c&d");   // a%20b/c&d  — leaves / and & unencoded

HttpUtility.UrlPathEncode sounds like the right tool but is not: it only encodes spaces and non-ASCII characters and leaves /, &, ? and # as they are. Microsoft's documentation for it says "Do not use; intended only for browser compatibility."

Decoding, and the + problem

Decoding has the mirror-image trap. Uri.UnescapeDataString does not treat + as a space, because in RFC 3986 a + is just a plus sign. WebUtility.UrlDecode and HttpUtility.UrlDecode do convert it:

Uri.UnescapeDataString("R%26B%20caf%C3%A9+x");   // R&B café+x
WebUtility.UrlDecode("R%26B+caf%C3%A9");         // R&B café

Decode with the counterpart of the encoder that produced the string. For data that came from an HTML form or a query string written by a browser form, use WebUtility.UrlDecode. In ASP.NET Core you rarely decode by hand at all: model binding and Request.Query already return decoded values, and decoding them again turns a user's literal %25 into % and can open injection bugs.

Obsolete and surprising APIs

Uri.EscapeUriString is marked obsolete since .NET 6 with warning SYSLIB0013. It was meant to encode a whole URL, so it leaves reserved characters alone, and that makes it unsafe for values: EscapeUriString("https://x.test/?q=R&B café#x") gives https://x.test/?q=R&B%20caf%C3%A9#x, with the ampersand and the fragment marker untouched. Replace it with EscapeDataString applied to each component.

Uri.ToString() returns a display form that unescapes some characters. For https://api.test/s?q=a%20b%26c it returns …?q=a b%26c with a real space, while AbsoluteUri returns the escaped form. Use AbsoluteUri or OriginalString whenever the URL is logged, signed or sent on.

Encoding twice is the most common bug of all: EscapeDataString(EscapeDataString("&")) is %2526. It usually happens when a value encoded by hand is passed to a helper such as QueryHelpers that encodes again. Paste a suspicious value into a URL decoder to count the layers.

Sources and further reading

Microsoft Learn documents Uri.EscapeDataString, Uri.UnescapeDataString, WebUtility.UrlEncode, HttpUtility.UrlEncode and UrlPathEncode, QueryHelpers.AddQueryString, and the SYSLIB0013 obsoletion of Uri.EscapeUriString. RFC 3986 defines unreserved characters and recommends upper-case hex digits. The WHATWG URL Standard defines application/x-www-form-urlencoded, which uses + for spaces. RFC 5849 (OAuth 1.0) and the AWS Signature Version 4 documentation specify the strict encoding signatures require.

Frequently asked questions

How do I URL encode a string in C#?

Use Uri.EscapeDataString(value). It is available on every .NET version, encodes the string as UTF-8, uses %20 for spaces and upper-case hex, and leaves only the RFC 3986 unreserved characters unencoded.

What is the difference between Uri.EscapeDataString and WebUtility.UrlEncode?

EscapeDataString follows RFC 3986 and encodes spaces as %20. WebUtility.UrlEncode produces HTML form encoding: spaces become +, and it leaves ( ) * ! unencoded while encoding ~.

Should I use HttpUtility.UrlEncode in .NET Core?

It works, but it writes lower-case hex and + for spaces, unlike the other encoders. Prefer Uri.EscapeDataString, or WebUtility.UrlEncode when you need form encoding.

Why is Uri.EscapeUriString obsolete?

It was designed for whole URLs, so it leaves reserved characters such as &, ? and # unencoded, which corrupts values. It is marked obsolete since .NET 6 (SYSLIB0013); use EscapeDataString on each component instead.

How do I build a query string in C#?

In ASP.NET Core use QueryHelpers.AddQueryString with a dictionary. Elsewhere, encode each key and value with Uri.EscapeDataString, join them with & and assign the result to UriBuilder.Query.

Why does Uri.UnescapeDataString not turn + into a space?

In RFC 3986 a + is a literal plus sign, so UnescapeDataString leaves it. Use WebUtility.UrlDecode for form-encoded data, where + means a space.