La codificación de URL, o codificación porcentual (percent-encoding), sustituye los caracteres que una URL no puede llevar tal cual por un % y el valor hexadecimal de sus bytes, de modo que un espacio se convierte en %20 y un & dentro de un valor deja de comportarse como separador.

En .NET tenemos cuatro formas de hacerlo: HttpUtility.UrlEncode(), WebUtility.UrlEncode(), Uri.EscapeDataString() y UrlEncoder.Default.Encode(). Producen salidas distintas para la misma entrada, y esa es la parte que a más de uno le hace perder una tarde. La herramienta para codificar y decodificar URL muestra, una junto a otra, las salidas de los cuatro métodos (HttpUtility, WebUtility, Uri.EscapeDataString y UrlEncoder) para la misma entrada, de modo que las diferencias se ven sin ejecutar este código.

Uri.EscapeDataString() es la opción por defecto que vale la pena memorizar. Sigue la RFC 3986, codifica un espacio como %20 en lugar de + y es lo más parecido que tiene C# a la función encodeURIComponent() de JavaScript.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

¿Qué es la codificación de URI?

La codificación de URI, también llamada codificación porcentual, sustituye los caracteres que una URL no puede llevar tal cual por un signo de porcentaje seguido del valor hexadecimal de sus bytes en UTF-8. Un espacio se convierte en %20 y un signo de interrogación, en %3F.

La regla procede de la RFC 3986, que divide los caracteres en dos grupos. Los caracteres no reservados, es decir, las letras ASCII, los dígitos y -, _, . y ~, viajan tal cual. Los caracteres reservados, como ?, &, =, / y #, tienen un significado estructural y marcan dónde termina una parte de la URL y dónde empieza la siguiente.

En ese significado estructural está todo el problema. Un valor que contenga & o = partiría la cadena de consulta por el lugar equivocado, así que lo codificamos a la ida y lo decodificamos a la vuelta.

La codificación se aplica a un solo valor, no a una dirección entera. Codificar una URL completa destruye los separadores que la URL necesita para seguir siendo legible.

Podemos clasificar los caracteres de una URL en reservados y no reservados. Los caracteres reservados son los que tienen un significado especial y, por lo general, marcan las distintas partes de una URL. Por ejemplo, el carácter ‘?‘ de una URL indica el inicio de los parámetros de consulta, si los hay.

La RFC3986 define qué caracteres son reservados y cuáles no.

Caracteres reservados:

! # $ & ‘ ( ) * + , / : ; = ? @ [ ]

Caracteres no reservados:

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 – _ . ~

Tenemos que codificar los caracteres reservados de una URL. Para ello, tomamos el valor hexadecimal del byte ASCII del carácter, precedido de un carácter ‘%‘. Por ejemplo, un espacio se codifica como ‘%20‘. Otro nombre de la codificación de URL es codificación porcentual, por el carácter ‘%‘ que se usa como prefijo.

Un problema relacionado, pero distinto, es el de la codificación Base64, que resuelve otro problema de transporte: la codificación porcentual hace que un valor sea seguro dentro de una URL, mientras que Base64 permite que bytes arbitrarios sobrevivan a un canal que solo admite texto.

Esta codificación es sencilla de hacer, pero, en lugar de escribir nosotros el código, podemos usar las distintas formas de codificar y decodificar que nos ofrece .NET.

¿Cómo codificar y decodificar una URL con HttpUtility?

HttpUtility está en el espacio de nombres System.Web y expone UrlEncode() y UrlDecode(). Cada uno recibe un string y devuelve la forma codificada o decodificada, y cada uno tiene sobrecargas para usar otro Encoding y para recibir una entrada byte[].

Dos de sus comportamientos sorprenden a más de uno. Codifica un espacio como + en lugar de %20, lo cual es correcto para el cuerpo de un formulario e incorrecto para un segmento de ruta. Además, escribe los dígitos hexadecimales en minúscula, así que ? sale como %3f y no como %3F.

La clase sigue disponible en las versiones actuales de .NET. Pese al nombre System.Web, se distribuye en el framework compartido y no con ASP.NET, así que una aplicación de consola normal puede llamarla sin añadir ningún paquete.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

La propia Microsoft recomienda preferir WebUtility fuera de una aplicación web, que es lo que trata el siguiente apartado. Eso deja a HttpUtility como la opción que conservamos para el código ASP.NET existente, no como la que elegimos para código nuevo.

En estos ejemplos codificamos una URL entera para que las diferencias entre los codificadores se vean en una sola cadena. En código real codificamos un único valor, no la dirección de la que forma parte:

using System.Net;
using System.Text.Encodings.Web;
using System.Web;

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

Estos métodos reciben un único parámetro string con la URL que hay que codificar o decodificar. De forma predeterminada, usan la codificación UTF-8, pero, si no es la que necesitamos, hay una sobrecarga para pasar otra codificación. También hay otras sobrecargas para pasar un Byte[] en lugar de un string.

¿Cómo codificar y decodificar una URL con WebUtility?

La referencia de HttpUtility.UrlEncode en Microsoft Learn lo resume en una línea: “Para codificar o decodificar valores fuera de una aplicación web, usa la clase WebUtility”. Esta clase está en el espacio de nombres System.Net.

Su uso es muy parecido al de los ejemplos anteriores, aunque no tiene sobrecargas:

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

¿Cómo usar Uri.EscapeDataString() y UnescapeDataString()?

Uri.EscapeDataString() aplica la codificación porcentual a un único valor y Uri.UnescapeDataString() la deshace. Los dos son estáticos, así que nunca creamos una instancia de Uri para llamarlos.

La diferencia más visible respecto a las dos clases de utilidades está en el carácter de espacio. EscapeDataString() escribe %20, y escribe los dígitos hexadecimales en mayúscula, que es lo que recomienda la RFC 3986. Por eso es la opción adecuada para los segmentos de ruta y para cualquier valor que tenga que coincidir byte a byte con la salida de otro sistema.

Esta asimetría es una trampa habitual. UnescapeDataString() no vuelve a convertir + en un espacio, así que un valor codificado con HttpUtility.UrlEncode() y decodificado aquí sigue teniendo los signos más. Las pruebas del proyecto de ejemplo verifican exactamente eso.

En .NET 9 y versiones posteriores hay sobrecargas basadas en span, EscapeDataString(ReadOnlySpan<char>) y TryEscapeDataString(), que codifican un span sin copiarlo antes en una cadena. UnescapeDataString() recibió el par equivalente al mismo tiempo, así que el código crítico para el rendimiento puede codificar y decodificar directamente en un búfer que gestionamos nosotros.

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

En código real codificamos el valor y dejamos la dirección como está, que es también como construimos la cadena de consulta en la que van esos valores codificados:

var searchUrl = $"https://example.com/search?q={Uri.EscapeDataString("bar with space")}";
Console.WriteLine(searchUrl); //https://example.com/search?q=bar%20with%20space

Cuando la cadena de consulta ya existe, usamos ese mismo codificador para añadir o actualizar un valor en una cadena de consulta existente sin romper los separadores que lo rodean.

¿Cómo codificar una URL con UrlEncoder en ASP.NET Core?

UrlEncoder está en System.Text.Encodings.Web y es el codificador que usa el propio pipeline de salida de ASP.NET Core. Para un uso puntual, llamamos a UrlEncoder.Default.Encode(). De forma predeterminada, no está en el contenedor de inyección de dependencias, así que una Web API tiene que recurrir a la propiedad estática o llamar antes a AddWebEncoders().

Existe por un motivo distinto al de los otros tres. HttpUtility y WebUtility se escribieron para que las URL funcionen, y UrlEncoder, para que la salida sea segura en el contexto que elijamos, por lo que es el único codificador cuyo comportamiento podemos configurar.

No tiene un decodificador equivalente. UrlEncoder es unidireccional por diseño, y la decodificación sigue pasando por Uri.UnescapeDataString() o WebUtility.UrlDecode().

Su lista de caracteres seguros es la más amplia de las cuatro, no la más estrecha. Deja intactos !, (, ), *, @, $, , y ;, mientras que Uri.EscapeDataString() los escapa. Por eso conviene recurrir a EscapeDataString() cuando el valor va en una URL que montamos nosotros, y construir un UrlEncoder a partir de un TextEncoderSettings cuando necesitamos otra lista de caracteres seguros.

var urlEncoderEncoded = UrlEncoder.Default.Encode(url);
Console.WriteLine(urlEncoderEncoded); //http%3A%2F%2Fexample.com%2Fresource%3Ffoo%3Dbar%20with%20space%23fragment

En el servidor, la parte equivalente de la decodificación es cómo leemos los valores de la cadena de consulta en el servidor, donde el framework ya los ha decodificado por nosotros.

¿Qué método de codificación de URL conviene usar en C#?

No hay una única respuesta correcta, pero sí una corta: usar Uri.EscapeDataString() para los valores que ponemos en una URL y WebUtility para el viaje de ida y vuelta de los datos de formulario.

Las cuatro opciones se diferencian en dos aspectos visibles. HttpUtility.UrlEncode() y WebUtility.UrlEncode() codifican un espacio como +, mientras que Uri.EscapeDataString() y UrlEncoder escriben %20. HttpUtility escribe los dígitos hexadecimales en minúscula y las otras tres, en mayúscula, así que ? se convierte en %3f en una y en %3F en las demás.

También se diferencian en cuánto codifican. Si lo medimos sobre todos los caracteres ASCII imprimibles, Uri.EscapeDataString() es el que más escapa, las dos clases de utilidades quedan en medio y UrlEncoder es el que menos escapa.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Hay una vieja advertencia que ya no se aplica. El límite de 65 519 caracteres de Uri.EscapeDataString() corresponde a .NET Framework y .NET Core, y en .NET 5 y versiones posteriores no existe. .NET 10 fue más allá y eliminó el límite de longitud total de la propia clase Uri, así que una cadena de consulta larga ya no es motivo para elegir un método en lugar de otro.

MétodoEspacio de nombresEl espacio se convierte enDígitos hexadecimalesDecodificar conÚsalo cuando
HttpUtility.UrlEncode()System.Web+minúsculasHttpUtility.UrlDecode()El código ASP.NET existente ya lo usa
WebUtility.UrlEncode()System.Net+mayúsculasWebUtility.UrlDecode()Codificar y decodificar valores de formulario
Uri.EscapeDataString()System%20mayúsculasUri.UnescapeDataString()Poner un valor en una URL
UrlEncoder.Default.Encode()System.Text.Encodings.Web%20mayúsculassin decodificador; usa Uri.UnescapeDataString()El valor codificado acaba en el marcado

¿Cuál es el equivalente en C# de encodeURIComponent()?

Uri.EscapeDataString() es el equivalente en C# más cercano a la función encodeURIComponent() de JavaScript. Los dos aplican la codificación porcentual a un único valor, los dos escriben %20 para un espacio y los dos usan dígitos hexadecimales en mayúscula.

Se parecen, pero no son idénticos. encodeURIComponent() deja intactos !, ', (, ) y *, y Uri.EscapeDataString() los codifica. Ambos resultados son válidos, porque codificar esos caracteres es opcional, pero, si comparamos las dos cadenas, no coinciden carácter a carácter.

En la decodificación, la correspondencia es la misma. decodeURIComponent() equivale a Uri.UnescapeDataString(), y ninguno de los dos vuelve a convertir + en un espacio.

HttpUtility.UrlEncode() y WebUtility.UrlEncode() no son el equivalente, porque sí codifican un espacio como +. Un valor que llega de un cliente JavaScript debe decodificarse con Uri.UnescapeDataString() para que coincida.

El sentido contrario importa igual. Un valor que codificamos con HttpUtility.UrlEncode() y entregamos a un navegador necesita decodeURIComponent() al otro lado, y cada espacio que llegó como + sobrevive al viaje de ida y vuelta como un signo más literal.

¿Por qué new Uri() lanza una UriFormatException?

El constructor Uri espera un URI absoluto, y las dos cadenas que la gente señala como fallos no se comportan igual.

new Uri("//example.com") no lanza ninguna excepción. Tanto en Windows como en Linux, la llamada funciona y devuelve file://example.com/, porque interpreta la doble barra inicial como el comienzo de un host, y Uri.TryCreate() también indica éxito. Hay que anteponer el esquema que realmente queremos antes de construir el Uri.

new Uri("/foo") depende de la plataforma. En Windows lanza una UriFormatException, porque una ruta sin más no es un URI absoluto. En Linux y macOS, la misma llamada funciona y devuelve file:///foo, porque allí una ruta sin más es una ruta local que parte de la raíz.

Para una ruta relativa, pasa UriKind.Relative o UriKind.RelativeOrAbsolute como segundo argumento, y el comportamiento es el mismo en todas partes. Uri.TryCreate() indica el fallo en lugar de lanzar una excepción, que es lo que queremos con una cadena que no hemos escrito nosotros.

Nada de esto tiene que ver con la codificación: EscapeDataString() y UnescapeDataString() son estáticos, así que codificar un valor nunca provoca esta excepción.

La RFC 3986 llama a la forma //example.com referencia de ruta de red (network-path reference), y es una forma real en el marcado, no una errata; por eso llega hasta el constructor. Cuando la cadena viene de fuera de nuestro código, el hábito más seguro es comprobar si una URL es válida antes de usarla, en lugar de dejar que decida el constructor.

Conclusión

Hemos visto en qué consiste la codificación de URL y hemos descubierto que .NET ofrece varias implementaciones para codificar y decodificar. Estos métodos se diferencian ligeramente en cómo codifican y decodifican las URL. La implementación que elijamos dependerá de nuestras necesidades concretas.

Probado con .NET 10.0.10.