Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Uri

new Uri(url) · new Uri(baseUri, relative) · Uri.TryCreate(text, UriKind.Absolute, out var u) · Uri.IsWellFormedUriString(text, kind) · u.GetLeftPart(UriPartial) · .Scheme / .Host / .Port / .AbsolutePath / .PathAndQuery / .Query / .Fragment

Parse a URL into its parts with the C#-faithful `new Uri(url)` handle. Construct it from a URL string, then read `.Scheme`, `.Host`, `.Port`, `.AbsolutePath`, `.PathAndQuery`, `.Query`, and `.Fragment`; resolve a relative reference with `new Uri(baseUri, "…")`; test a text without an exception with `Uri.TryCreate` or `Uri.IsWellFormedUriString`; cut a URL down to its origin with `u.GetLeftPart(UriPartial.Authority)`. Pure — no capability needed — and it runs in the browser too, answering what the server answers. Pairs with the HTTP surface (parse a webhook or redirect URL).

stable7 examples compiled by CIuriurlparsingbcl

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#

Related

Encoding — Base64, hex, text encodings, URL, HTML

Encode and decode text — Base64, URL percent-encoding, and HTML escaping — with the C#-faithful spellings…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…

Regex

Match, replace, split and CAPTURE with regular expressions — the C#-faithful System.Text.RegularExpressions spelling…

Comparing classes

`==` on two `class` values asks whether they are the SAME INSTANCE, exactly as C# does: two objects built separately…