.NET provides System.Uri for URL parsing and UriBuilder for URL construction. These classes offer robust, RFC-compliant URL handling for C# applications.
Key Takeaways
- 1System.Uri parses and validates URLs according to RFC 3986
- 2UriBuilder provides a mutable way to construct URLs
- 3HttpUtility.ParseQueryString returns a NameValueCollection
- 4Uri.TryCreate is the safe way to parse untrusted URLs
- 5Use WebUtility for encoding in modern .NET applications
“A URI is a compact representation of a resource available to your application on the intranet or internet. The Uri class defines the properties and methods for handling URIs, including parsing, comparing, and combining.”
Parsing URLs with Uri
The Uri class parses URLs and provides properties for each component. Unlike JavaScript's URL which throws on invalid input, you can use Uri.TryCreate() for safe parsing of untrusted input.
using System;
var url = "https://user:pass@example.com:8080/path/page?query=value#fragment";
var uri = new Uri(url);
Console.WriteLine($"Scheme: {uri.Scheme}"); // https
Console.WriteLine($"Host: {uri.Host}"); // example.com
Console.WriteLine($"Port: {uri.Port}"); // 8080
Console.WriteLine($"Path: {uri.AbsolutePath}"); // /path/page
Console.WriteLine($"Query: {uri.Query}"); // ?query=value
Console.WriteLine($"Fragment: {uri.Fragment}"); // #fragment
Console.WriteLine($"UserInfo: {uri.UserInfo}"); // user:pass
Console.WriteLine($"Authority: {uri.Authority}"); // user:pass@example.com:8080
// Path segments
foreach (var segment in uri.Segments)
{
Console.WriteLine($"Segment: {segment}");
}
// Segment: /
// Segment: path/
// Segment: pageThe code shows the most commonly used properties. Notice that Query includes the leading ? and Fragment includes the #. The Segments property breaks the path into an array, which is useful for routing.
Uri Properties
The Uri class provides many properties for different use cases. Here are the most useful ones.
| Property | Type | Description |
|---|---|---|
| Scheme | string | Protocol (http, https) |
| Host | string | Domain name or IP |
| Port | int | Port number |
| AbsolutePath | string | Path without query |
| PathAndQuery | string | Path with query string |
| Query | string | Query string with ? |
| Fragment | string | Fragment with # |
| UserInfo | string | user:pass portion |
| Authority | string | userinfo@host:port |
| AbsoluteUri | string | Complete URL |
The distinction between AbsolutePath and PathAndQuery is useful: use AbsolutePath when you need the path alone, and PathAndQuery when building relative URLs or routing.
Parsing Query Parameters
.NET's HttpUtility.ParseQueryString() returns a NameValueCollection that handles duplicate keys automatically. This is similar to JavaScript's URLSearchParams.
using System;
using System.Web; // System.Web for .NET Framework
// For .NET Core/5+, use Microsoft.AspNetCore.WebUtilities
var uri = new Uri("https://shop.com/search?category=books&tag=csharp&tag=dotnet");
// Parse query string into NameValueCollection
var query = HttpUtility.ParseQueryString(uri.Query);
Console.WriteLine(query["category"]); // books
Console.WriteLine(query["tag"]); // csharp (first value)
Console.WriteLine(query.GetValues("tag")); // ["csharp", "dotnet"]
// Iterate all parameters
foreach (string key in query.AllKeys)
{
var values = query.GetValues(key);
Console.WriteLine($"{key}: {string.Join(", ", values)}");
}
// category: books
// tag: csharp, dotnet
// Check if parameter exists
if (query.AllKeys.Contains("category"))
{
Console.WriteLine("Has category parameter");
}The NameValueCollection handles duplicate keys by storing all values. Use GetValues() to get all values as an array, or the indexer to get the first value. Note that AllKeys returns unique keys only.
Building URLs with UriBuilder
UriBuilder provides a mutable way to construct URLs piece by piece. Combined with ParseQueryString(), it makes URL manipulation straightforward.
using System;
using System.Web;
// Basic URL building
var builder = new UriBuilder
{
Scheme = "https",
Host = "api.example.com",
Port = 443,
Path = "/v2/users"
};
Console.WriteLine(builder.Uri);
// https://api.example.com/v2/users
// Building with query parameters
var queryBuilder = new UriBuilder("https://api.example.com/search");
var query = HttpUtility.ParseQueryString(string.Empty);
query["q"] = "c# tutorial";
query["page"] = "1";
query["sort"] = "relevance";
queryBuilder.Query = query.ToString();
Console.WriteLine(queryBuilder.Uri);
// https://api.example.com/search?q=c%23+tutorial&page=1&sort=relevance
// Modifying existing URL
var existing = new UriBuilder("https://example.com/path?existing=value");
var existingQuery = HttpUtility.ParseQueryString(existing.Query);
existingQuery["new"] = "param";
existingQuery["existing"] = "updated";
existing.Query = existingQuery.ToString();
Console.WriteLine(existing.Uri);
// https://example.com/path?existing=updated&new=paramA useful pattern: call ParseQueryString(string.Empty) to get an empty NameValueCollection with query string formatting. Setting the indexer adds or updates parameters, and ToString() produces the encoded query string.
Modifying Query Parameters
These extension methods make URL manipulation more ergonomic. They follow the pattern of parsing, modifying, and rebuilding.
using System;
using System.Web;
public static class UriExtensions
{
public static Uri SetQueryParam(this Uri uri, string key, string value)
{
var builder = new UriBuilder(uri);
var query = HttpUtility.ParseQueryString(builder.Query);
query[key] = value;
builder.Query = query.ToString();
return builder.Uri;
}
public static Uri RemoveQueryParam(this Uri uri, string key)
{
var builder = new UriBuilder(uri);
var query = HttpUtility.ParseQueryString(builder.Query);
query.Remove(key);
builder.Query = query.ToString();
return builder.Uri;
}
public static Uri AddQueryParams(this Uri uri, IDictionary<string, string> @params)
{
var builder = new UriBuilder(uri);
var query = HttpUtility.ParseQueryString(builder.Query);
foreach (var kvp in @params)
{
query[kvp.Key] = kvp.Value;
}
builder.Query = query.ToString();
return builder.Uri;
}
}
// Usage
var uri = new Uri("https://api.com/search?q=test&page=1");
var updated = uri.SetQueryParam("page", "2");
Console.WriteLine(updated); // https://api.com/search?q=test&page=2
var removed = uri.RemoveQueryParam("page");
Console.WriteLine(removed); // https://api.com/search?q=test
var multi = uri.AddQueryParams(new Dictionary<string, string>
{
["sort"] = "date",
["limit"] = "20"
});
Console.WriteLine(multi); // https://api.com/search?q=test&page=1&sort=date&limit=20Extension methods make the API fluent: uri.SetQueryParam("key", "value") is cleaner than creating a builder manually each time. These return new Uri instances since Uri is immutable.
Resolving Relative URLs
C# handles relative URL resolution with the Uri constructor's two-argument form or the MakeRelativeUri() method for the reverse operation.
using System;
var baseUri = new Uri("https://example.com/docs/guide/");
// Resolve relative paths
var relative1 = new Uri(baseUri, "../api/reference");
Console.WriteLine(relative1);
// https://example.com/docs/api/reference
var relative2 = new Uri(baseUri, "page.html");
Console.WriteLine(relative2);
// https://example.com/docs/guide/page.html
var absolute = new Uri(baseUri, "/about");
Console.WriteLine(absolute);
// https://example.com/about
// Make relative from two absolute URLs
var target = new Uri("https://example.com/docs/api/reference");
var relativized = baseUri.MakeRelativeUri(target);
Console.WriteLine(relativized);
// ../api/referenceThe two-argument Uri constructor resolves the second argument against the first base URL. The MakeRelativeUri() method is useful when generating links that should work relative to a base path.
URL Encoding
.NET provides multiple encoding methods, each suited for different scenarios. Understanding when to use each prevents common encoding bugs.
using System;
using System.Net;
using System.Web;
// WebUtility (recommended for .NET Core/5+)
var encoded = WebUtility.UrlEncode("hello world & friends");
Console.WriteLine(encoded); // hello+world+%26+friends
var decoded = WebUtility.UrlDecode(encoded);
Console.WriteLine(decoded); // hello world & friends
// HttpUtility (for ASP.NET)
var htmlEncoded = HttpUtility.UrlEncode("hello world");
Console.WriteLine(htmlEncoded); // hello+world
// Uri.EscapeDataString (RFC 3986 compliant)
var rfcEncoded = Uri.EscapeDataString("hello world & friends");
Console.WriteLine(rfcEncoded); // hello%20world%20%26%20friends
// Uri.EscapeUriString (for complete URIs, less strict)
var uriEncoded = Uri.EscapeUriString("https://example.com/path with spaces");
Console.WriteLine(uriEncoded); // https://example.com/path%20with%20spaces
// Decoding
var rfcDecoded = Uri.UnescapeDataString("hello%20world");
Console.WriteLine(rfcDecoded); // hello worldThe key differences: WebUtility.UrlEncode() and HttpUtility.UrlEncode() encode spaces as + (form encoding), while Uri.EscapeDataString() uses %20 (RFC 3986). Use the latter for path segments and the former for query string values.
URL Validation
Uri.TryCreate() is the safe way to validate URLs without throwing exceptions. Combine it with scheme and host checks for proper security validation.
using System;
public static class UrlValidator
{
public static bool IsValidHttpUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
return false;
}
return uri.Scheme == Uri.UriSchemeHttp ||
uri.Scheme == Uri.UriSchemeHttps;
}
public static bool IsSafeRedirect(string url, string[] allowedHosts)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
return false;
}
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
{
return false;
}
return allowedHosts.Any(host =>
uri.Host.Equals(host, StringComparison.OrdinalIgnoreCase) ||
uri.Host.EndsWith("." + host, StringComparison.OrdinalIgnoreCase));
}
public static bool IsLocalUrl(string url)
{
// Safe for redirect: relative paths and absolute paths without host
if (Uri.TryCreate(url, UriKind.Relative, out _))
{
return !url.StartsWith("//"); // Protocol-relative URLs are not local
}
return false;
}
}
// Usage
Console.WriteLine(UrlValidator.IsValidHttpUrl("https://example.com")); // True
Console.WriteLine(UrlValidator.IsValidHttpUrl("javascript:alert(1)")); // False
Console.WriteLine(UrlValidator.IsValidHttpUrl("file:///etc/passwd")); // False
var allowed = new[] { "myapp.com", "api.myapp.com" };
Console.WriteLine(UrlValidator.IsSafeRedirect("https://myapp.com/cb", allowed)); // True
Console.WriteLine(UrlValidator.IsSafeRedirect("https://evil.com/cb", allowed)); // FalseThe static validator class demonstrates good C# patterns: using TryCreate() for safe parsing, built-in scheme constants like Uri.UriSchemeHttps, and LINQ for clean host matching logic. The out var syntax keeps the code concise.
With HttpClient
HttpClient is the standard way to make HTTP requests in modern .NET. It works seamlessly with Uri and UriBuilder.
using System;
using System.Net.Http;
using System.Web;
var client = new HttpClient();
// Build URL with query parameters
var builder = new UriBuilder("https://api.example.com/search");
var query = HttpUtility.ParseQueryString(string.Empty);
query["q"] = "c# programming";
query["page"] = "1";
builder.Query = query.ToString();
var response = await client.GetAsync(builder.Uri);
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Status: {response.StatusCode}");
Console.WriteLine($"Final URL: {response.RequestMessage?.RequestUri}");
// POST with query parameters
var postBuilder = new UriBuilder("https://api.example.com/users");
var postQuery = HttpUtility.ParseQueryString(string.Empty);
postQuery["validate"] = "true";
postBuilder.Query = postQuery.ToString();
var postContent = new StringContent("{\"name\":\"John\"}",
System.Text.Encoding.UTF8, "application/json");
var postResponse = await client.PostAsync(postBuilder.Uri, postContent);The code shows both GET requests with query parameters and POST requests with URL parameters plus a JSON body. Use builder.Uri to get the immutable Uri from the builder. The RequestMessage?.RequestUri gives you the final URL after any redirects.