Summary#
Uri is the parsed-URL handle, spelled exactly as in C#. Construct it from a URL string and read its parts:
var u = new Uri("https://api.example.com:8443/v1/users?active=true#top");
u.Scheme; // "https"
u.Host; // "api.example.com"
u.Port; // 8443
u.AbsolutePath; // "/v1/users"
u.PathAndQuery; // "/v1/users?active=true"
u.Query; // "?active=true"
u.Fragment; // "#top"It is pure — no capability, no using required. Uri is also where the URL-escaping helpers live
(Encoding — Base64, hex, text encodings, URL, HTML — Uri.EscapeDataString), exactly as C#'s System.Uri carries both.
Signature#
Uri new Uri(string url) // parse an ABSOLUTE URL
Uri new Uri(string url, UriKind.Absolute) // the same, with the kind said
Uri new Uri(Uri baseUri, string relative) // resolve a relative reference ("../b?x=1") against a base
bool Uri.TryCreate(string? text, UriKind.Absolute, out Uri? u) // false, never a throw, for a text that is not a URL
bool Uri.TryCreate(Uri? baseUri, string? relative, out Uri? u) // resolve against a base; false where C# answers false
bool Uri.IsWellFormedUriString(string? text, UriKind kind) // Absolute · Relative · RelativeOrAbsolute
string uri.GetLeftPart(UriPartial part) // Scheme "https://" · Authority "https://host:8443" · Path · Query
string uri.Scheme // "https"
string uri.Host // "api.example.com"
int uri.Port // 8443 (or the scheme default: 443 for https, 80 for http)
string uri.AbsolutePath // "/v1/users"
string uri.PathAndQuery // "/v1/users?active=true"
string uri.Query // "?active=true" (empty when there is none)
string uri.Fragment // "#top" (empty when there is none)
string uri.AbsoluteUri // the whole URL, canonical and escaped
string uri.ToString() // the canonical form, readable escapes undone ("…/a b") — also $"{uri}"
string uri.OriginalString // exactly the text it was made from
string uri.Authority // "api.example.com:8443" — the port only when it is not the default
string uri.LocalPath // the path, unescaped
string uri.UserInfo // "user:password" before the host, or empty
string uri.IdnHost // the host in ASCII (punycode)
string uri.DnsSafeHost // the host without IPv6 brackets
string[] uri.Segments // ["/", "v1/", "users"]
bool uri.IsDefaultPort · uri.IsFile · uri.IsLoopback · uri.IsAbsoluteUri
bool a == b · a != b · a.Equals(b) // the same address, as C# compares two Uris
string Uri.UriSchemeHttps // "https"
string Uri.UriSchemeHttp // "http"
string Uri.UriSchemeFtp // "ftp"
string Uri.UriSchemeFile // "file"
string Uri.UriSchemeMailto // "mailto"
string Uri.UriSchemeWs // "ws"
string Uri.UriSchemeWss // "wss"
string Uri.SchemeDelimiter // "://"Description#
new Uri(url) parses an absolute URL once and exposes its components as instance members — the same shape as C#'s
System.Uri. When the URL omits the port, .Port is the scheme's default (443 for https, 80 for http). .Query
and .Fragment include their leading ? / #, and are empty strings when absent.
An invalid or relative URL throws C#'s UriFormatException (a FormatException), with C#'s own message —
catch (UriFormatException e) takes it. new Uri parses an absolute URI, faithful to C#; to parse a relative one,
resolve it against a base: new Uri(baseUri, "orders/42") follows RFC 3986 exactly as System.Uri does (../
climbs, a leading / replaces the path, //host replaces the authority, ?x keeps the path).
To test a text without an exception, Uri.TryCreate(text, UriKind.Absolute, out var u) answers false (and
u is null) where new Uri(text) would throw, and Uri.TryCreate(baseUri, relative, out var u) does the same for a
reference resolved against a base — a null base resolves only an absolute reference, as in C#.
Uri.IsWellFormedUriString(text, kind) is C#'s stricter question: whether the text is already written as a URL should
be — escaped, no stray spaces or backslashes — so "http://a b" is not well-formed although new Uri accepts it.
UriKind.Relative asks whether it is a well-formed relative reference ("orders/42?x=1").
u.GetLeftPart(part) is the URL up to and including a part: UriPartial.Scheme is "https://", Authority the
origin ("https://api.example.com:8443"), Path adds the path and Query the query — never the fragment.
UriKind.Absolute is accepted and means what new Uri(url) already means. In new Uri(text, kind) and
Uri.TryCreate(text, kind, out var u), UriKind.Relative and UriKind.RelativeOrAbsolute are refused by name: a relative Uri has no scheme or host to read, so the refusal points
at new Uri(baseUri, "…"), which is what such a value is usually for.
One difference from .NET, on purpose: every member is read when the Uri is MADE, so where System.Uri accepts a host
and only throws later from IdnHost (a name IDN refuses), Osy# throws the same UriFormatException from new Uri,
and Uri.TryCreate answers false for it.
Every member answers what C#'s System.Uri answers, because the server parses with it: dot segments are removed
(/a/../c is /c), the scheme and host are lower-cased, a default port is dropped from AbsoluteUri, and
ToString() is the readable form where AbsoluteUri is the escaped one (/a b against /a%20b).
Two Uris are equal when they name the same address, which is C#'s rule and not "the same text": the scheme
and host ignore case, a default port is the same as none, and the fragment is not compared. ==, !=, Equals,
Distinct() and a Dictionary<Uri, …> key all answer by it.
A Uri runs in the browser too, and answers what the server answers. The client carries a port of .NET's URI
rules — which characters it escapes, unescapes and keeps, IPv4 in every numeric form, IPv6, IDN hosts, user info, the
per-scheme rules (http, ftp, ldap, net.tcp, …), mailto:, file: with Windows and UNC paths — held to
System.Uri over well over a thousand texts on both sides. So a page action can parse, compare and resolve a URL
without a round trip, and a Uri used as a Dictionary key or HashSet element compares by address there as it
does on the server.
Examples#
Route on the host of a webhook URL, parsing an Http.* target:
bool IsTrustedHost(string webhookUrl) {
var u = new Uri(webhookUrl);
return u.Scheme == "https" && Text.EndsWith(u.Host, ".example.com");
}Build a URL with an escaped query value (Encoding — Base64, hex, text encodings, URL, HTML) and read it back:
Uri Search(string term) {
return new Uri("https://api.example.com/search?q=" + Uri.EscapeDataString(term));
}Normalize a URL the way C# does before storing it:
/// "https://example.com/c?x=1" — scheme and host lower-cased, `..` resolved, the default port dropped.
string Canonical(string url) => new Uri(url).AbsoluteUri;
/// "/a b" — the path with its escapes undone, where AbsolutePath keeps "/a%20b".
string Readable(string url) => new Uri(url).LocalPath;
bool IsSecure(string url) => new Uri(url).Scheme == Uri.UriSchemeHttps;Compare two addresses, however each was written:
/// true for "HTTPS://Example.com:443/docs#intro" and "https://example.com/docs" — one address.
bool SameAddress(string a, string b) => new Uri(a) == new Uri(b);
/// How many different addresses a list of links holds.
int DistinctAddresses(List<string> links) => links.Select(l => new Uri(l)).Distinct().Count();Resolve a link found in a page against the page's own address, and refuse what is not a URL:
/// "https://example.com/docs/guide/next" for ("https://example.com/docs/guide/intro", "next"),
/// "https://example.com/about" for "/about", and "" for a base that is not a URL at all.
string Resolve(string pageUrl, string href) {
try {
return new Uri(new Uri(pageUrl), href).AbsoluteUri;
} catch (UriFormatException) {
return "";
}
}Take a link from user input only when it is a web address, and keep only its origin:
/// "https://shop.example.com" for "https://shop.example.com/cart?id=7", and "" for "not a url" or "ftp://x".
string Origin(string text) {
if (Uri.TryCreate(text, UriKind.Absolute, out var u) && (u.Scheme == Uri.UriSchemeHttps || u.Scheme == Uri.UriSchemeHttp))
return u.GetLeftPart(UriPartial.Authority);
return "";
}
/// Whether a stored link is written as a URL should be — escaped, absolute or relative.
bool IsTidyLink(string link) => Uri.IsWellFormedUriString(link, UriKind.RelativeOrAbsolute);
/// The page a link points at, resolved against the page it was found on, or null.
Uri? Target(Uri page, string href) => Uri.TryCreate(page, href, out var t) ? t : null;Every scheme name C# gives a constant for, and the resolving and testing forms, pinned:
[Test]
void Uri_constants_and_forms_answer_what_csharp_answers() {
Assert.Equal("http", Uri.UriSchemeHttp);
Assert.Equal("https", Uri.UriSchemeHttps);
Assert.Equal("ftp", Uri.UriSchemeFtp);
Assert.Equal("file", Uri.UriSchemeFile);
Assert.Equal("mailto", Uri.UriSchemeMailto);
Assert.Equal("ws", Uri.UriSchemeWs);
Assert.Equal("wss", Uri.UriSchemeWss);
Assert.Equal("://", Uri.SchemeDelimiter);
var page = new Uri("https://example.com/docs/guide/intro");
Assert.Equal("https://example.com/docs/guide/next", new Uri(page, "next").AbsoluteUri);
Assert.True(Uri.TryCreate(page, "../about", out var about));
Assert.Equal("https://example.com/docs/about", about.AbsoluteUri);
Assert.False(Uri.TryCreate("not a url", UriKind.Absolute, out var nothing));
Assert.Null(nothing);
Assert.Equal("https://example.com", page.GetLeftPart(UriPartial.Authority));
}See also#
- Comparing classes — how
==compares a class, a record and a struct - Encoding — Base64, hex, text encodings, URL, HTML —
Uri.EscapeDataString/UnescapeDataStringfor building the URL you parse - Http.* — the outbound HTTP surface whose URLs this parses
- Regex — validate a URL string before parsing untrusted input