Updated on
URL encoding, or percent-encoding, replaces the characters a URL cannot carry literally with a % and the hexadecimal value of their bytes, so that a space becomes %20 and a & inside a value stops behaving like a separator.
.NET gives us four ways to do it: HttpUtility.UrlEncode(), WebUtility.UrlEncode(), Uri.EscapeDataString(), and UrlEncoder.Default.Encode(). They produce different output for the same input, which is the part that costs people an afternoon.
Uri.EscapeDataString() is the default worth memorising. It follows RFC 3986, it encodes a space as %20 rather than +, and it is the closest thing C# has to JavaScript’s encodeURIComponent().
What Is URI Encoding?
URI encoding, also called percent-encoding, replaces characters a URL cannot carry literally with a percent sign followed by the hexadecimal value of their UTF-8 bytes. A space becomes %20 and a question mark becomes %3F.
The rule comes from RFC 3986, which sorts characters into two groups. Unreserved characters, meaning the ASCII letters, the digits, and -, _, . and ~, travel as they are. Reserved characters such as ?, &, =, / and # carry structural meaning and mark where one part of a URL ends and the next begins.
That structural meaning is the whole problem. A value containing & or = would split a query string in the wrong place, so we encode it on the way in and decode it on the way out.
Encoding applies to a single value, not to a whole address. Encoding a complete URL destroys the separators the URL needs to stay readable.
We can classify characters in a URL as either reserved or unreserved. The reserved characters are those characters that have a special meaning and generally mark the various parts of a URL. For example, the ‘?‘ character in a URL indicates the start of any query parameters.
RFC3986 defines which characters are reserved and unreserved.
Reserved characters:
! # $ & ‘ ( ) * + , / : ; = ? @ [ ]
Unreserved characters:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9 – _ . ~
We need to encode reserved characters in a URL. To do this, we take the hexadecimal ASCII byte value of the character, preceded with a ‘%‘ character. For example, a space character encodes to ‘%20‘. Another name for URL encoding is percent-encoding, due to the ‘%‘ prefix character used.
A related but separate problem is Base64 encoding, which solves a different transport problem: percent-encoding makes a value safe inside a URL, while Base64 makes arbitrary bytes survive a text-only channel.
This encoding is simple to perform, but instead of writing code to do this ourselves, .NET provides a few ways of encoding and decoding for us.
How Do We Encode and Decode a URL With HttpUtility?
HttpUtility lives in the System.Web namespace and exposes UrlEncode() and UrlDecode(). Each takes a string and returns the encoded or decoded form, and each has overloads for a different Encoding and for byte[] input.
Two of its behaviours surprise people. It encodes a space as + rather than %20, which is right for a form body and wrong for a path segment. It also writes its hexadecimal digits in lower case, so ? comes back as %3f rather than %3F.
The class is still available on current .NET. Despite the System.Web name it ships in the shared framework rather than with ASP.NET, so a plain console application can call it without adding a package.
Microsoft’s own guidance is to prefer WebUtility outside a web application, which is what the next section covers. That leaves HttpUtility as the option we keep for existing ASP.NET code rather than the one we reach for in new code.
We are encoding a whole URL in these examples so the differences between the encoders are visible in one string. In real code we encode a single value, not the address it goes into:
var url = @"http://example.com/resource?foo=bar with space#fragment"; var httpUtilityEncoded = HttpUtility.UrlEncode(url); Console.WriteLine(httpUtilityEncoded); //http%3a%2f%2fexample.com%2fresource%3ffoo%3dbar+with+space%23fragment var httpUtilityDecoded = HttpUtility.UrlDecode(httpUtilityEncoded); Console.WriteLine(httpUtilityDecoded); //http://example.com/resource?foo=bar with space#fragment
These methods take a single string parameter containing the URL to be either encoded or decoded. By default, these methods use a UTF-8 encoding, but if this is not the case, there is an overload to pass a different encoding instead. There are also other method overloads to pass a Byte[] instead of a string type.
How Do We Encode and Decode a URL With WebUtility?
The documentation states that if we are not within a web application, we should use the WebUtility class to perform URL encoding and decoding instead. This class is in the System.Net namespace.
Usage is very similar to the previous examples, although there are no overloads:
var webUtilityEncoded = WebUtility.UrlEncode(url); Console.WriteLine(webUtilityEncoded); //http%3A%2F%2Fexample.com%2Fresource%3Ffoo%3Dbar+with+space%23fragment var webUtilityDecoded = WebUtility.UrlDecode(webUtilityEncoded); Console.WriteLine(webUtilityDecoded); //http://example.com/resource?foo=bar with space#fragment
How Do We Use Uri.EscapeDataString() and UnescapeDataString()?
Uri.EscapeDataString() percent-encodes a single value and Uri.UnescapeDataString() reverses it. Both are static, so we never create a Uri instance to call them.
The difference from the two utility classes is the space character. EscapeDataString() writes %20, and it writes hexadecimal digits in upper case, which is what RFC 3986 recommends. That makes it the right choice for path segments and for any value that has to match another system’s output byte for byte.
The asymmetry catches people out. UnescapeDataString() does not turn + back into a space, so a value encoded by HttpUtility.UrlEncode() and decoded here comes back with plus signs still in it. The sample project’s tests assert exactly that.
On .NET 9 and later there are span overloads, EscapeDataString(ReadOnlySpan<char>) and TryEscapeDataString(), for encoding without allocating a new string. UnescapeDataString() gained the matching pair at the same time, so a hot path can encode and decode straight into a buffer we own.
var uriEncoded = Uri.EscapeDataString(url); Console.WriteLine(uriEncoded); //http%3A%2F%2Fexample.com%2Fresource%3Ffoo%3Dbar%20with%20space%23fragment var uriDecoded = Uri.UnescapeDataString(uriEncoded); Console.WriteLine(uriDecoded); //http://example.com/resource?foo=bar with space#fragment
In real code we encode the value and leave the address alone, which is also how we build the query string those encoded values go into:
var searchUrl = $"https://example.com/search?q={Uri.EscapeDataString("bar with space")}";
Console.WriteLine(searchUrl); //https://example.com/search?q=bar%20with%20space
Once a query string already exists, the same encoder is what we use to append or update a value in an existing query string without breaking the separators around it.
How Do We Encode a URL With UrlEncoder in ASP.NET Core?
UrlEncoder lives in System.Text.Encodings.Web and is the encoder ASP.NET Core’s own output pipeline uses. We call UrlEncoder.Default.Encode() for a one-off. It is not in the dependency injection container by default, so a Web API has to reach for the static property or call AddWebEncoders() first.
It exists for a different reason than the other three. HttpUtility and WebUtility were written to make URLs work, and UrlEncoder was written to make output safe in a context we choose, which is why it is the one encoder whose behaviour we can configure.
There is no matching decoder. UrlEncoder is one-directional by design, and decoding still goes through Uri.UnescapeDataString() or WebUtility.UrlDecode().
Its safe list is the widest of the four, not the narrowest. It leaves !, (, ), *, @, $, , and ; alone where Uri.EscapeDataString() escapes them, so reach for EscapeDataString() when the value goes into a URL we are assembling ourselves, and build a UrlEncoder from a TextEncoderSettings when we need a different safe list.
var urlEncoderEncoded = UrlEncoder.Default.Encode(url); Console.WriteLine(urlEncoderEncoded); //http%3A%2F%2Fexample.com%2Fresource%3Ffoo%3Dbar%20with%20space%23fragment
On the server, the decode-side counterpart is how we read query string values back out on the server, where the framework has already decoded them for us.
Which URL Encoding Method Should We Use in C#?
There is no single correct answer, but there is a short one: use Uri.EscapeDataString() for values we put into a URL, and WebUtility for round-tripping form data.
The four options differ on two visible points. HttpUtility.UrlEncode() and WebUtility.UrlEncode() encode a space as +, while Uri.EscapeDataString() and UrlEncoder write %20. HttpUtility writes lower-case hexadecimal digits and the other three write upper case, so ? becomes %3f in one and %3F in the rest.
They also differ in how much they encode. Measured on one string, Uri.EscapeDataString() escapes the most, the two utility classes sit in the middle, and UrlEncoder escapes the fewest.
One old warning no longer applies. The 32766 character limit on Uri.EscapeDataString() belongs to .NET Framework, and on .NET 5 and later there is no such limit. .NET 10 went further and removed the remaining length limits on the Uri class itself, so a long query string is no longer a reason to pick one method over another.
| Method | Namespace | Space becomes | Hex digits | Decode with | Reach for it when |
|---|---|---|---|---|---|
HttpUtility.UrlEncode() | System.Web | + | lower case | HttpUtility.UrlDecode() | Existing ASP.NET code already uses it |
WebUtility.UrlEncode() | System.Net | + | upper case | WebUtility.UrlDecode() | Round-tripping form-encoded values |
Uri.EscapeDataString() | System | %20 | upper case | Uri.UnescapeDataString() | Putting a value into a URL |
UrlEncoder.Default.Encode() | System.Text.Encodings.Web | %20 | upper case | no decoder; use Uri.UnescapeDataString() | The encoded value lands in markup |
What Is the C# Equivalent of encodeURIComponent()?
Uri.EscapeDataString() is the closest C# equivalent of JavaScript’s encodeURIComponent(). Both percent-encode a single value, both write %20 for a space, and both use upper-case hexadecimal digits.
They are close rather than identical. encodeURIComponent() leaves !, ', (, ) and * alone, and Uri.EscapeDataString() encodes them. Both results are valid, because those characters are optional to encode, but the two strings do not match character for character if we compare them.
The decode direction pairs the same way. decodeURIComponent() corresponds to Uri.UnescapeDataString(), and neither of them converts + back into a space.
HttpUtility.UrlEncode() and WebUtility.UrlEncode() are not the equivalent, because they do encode a space as +. A value arriving from a JavaScript client should be decoded with Uri.UnescapeDataString() to match.
The reverse trip matters just as much. A value we encode with HttpUtility.UrlEncode() and hand to a browser needs decodeURIComponent() on the other side, and every space that arrived as + survives the round trip as a literal plus sign.
Why Does new Uri() Throw a UriFormatException?
The Uri constructor expects an absolute URI, and the two strings people report as failures do not behave alike.
new Uri("//example.com") does not throw at all. On Windows and on Linux alike it succeeds and hands back file://example.com/, reading the leading double slash as the start of a host, and Uri.TryCreate() reports success too. Prepend the scheme we actually want before constructing the Uri.
new Uri("/foo") depends on the platform. On Windows it throws a UriFormatException, because a bare path is not an absolute URI. On Linux and macOS the same call succeeds and yields file:///foo, because there a bare path is a rooted local path.
For a relative path, pass UriKind.Relative or UriKind.RelativeOrAbsolute as the second argument, which behaves the same everywhere. Uri.TryCreate() reports failure instead of throwing, which is what we want for a string we did not write ourselves.
None of this involves encoding: EscapeDataString() and UnescapeDataString() are static, so encoding a value never trips this exception.
RFC 3986 calls the //example.com shape a network-path reference, and it is a real form in markup rather than a typo, which is why it reaches the constructor at all. When the string comes from outside our own code, the safer habit is to check whether a URL is valid before we use it rather than to let the constructor decide.
Conclusion
We’ve learned about URL encoding and discovered there are multiple implementations in .NET to perform both encoding and decoding. These methods differ slightly in how they encode and decode our URLs. The particular implementation we choose will depend upon our specific requirements.
Tested with .NET 10.0.10.

There are actually a few others that could be of use to some people, with some examples here
https://stackoverflow.com/questions/575440/url-encoding-using-c-sharp/11236038#11236038