Matching URLs

edit

A UrlPattern holds one pattern per component of a URL: protocol, username, password, hostname, port, pathname, search and hash. A URL matches when every component matches its pattern.

Create a pattern from a string

Write the pattern as a URL. The components the string leaves out match anything:

var pattern = UrlPattern.Create("https://example.com/books/:id");

// Each component of the URL gets its own pattern
Assert.Equal("https", pattern.Protocol);
Assert.Equal("example.com", pattern.Hostname);
Assert.Equal("/books/:id", pattern.Pathname);

// A component the string does not mention matches anything
Assert.Equal("*", pattern.Search);
Assert.True(pattern.IsMatch("https://example.com/books/123?format=json#reviews"));

// A relative pattern needs a base URL, which supplies the missing components
var relativePattern = UrlPattern.Create("/books/:id", "https://example.com");
Assert.Equal("example.com", relativePattern.Hostname);
Assert.True(relativePattern.IsMatch("https://example.com/books/123"));

Create a pattern from components

A UrlPatternInit sets the pattern of each component separately. The components you do not set default to *, so the pattern below matches the path on any host:

// Only the pathname is constrained: every other component defaults to "*"
var pattern = UrlPattern.Create(new UrlPatternInit
{
    Pathname = "/books/:id",
});

Assert.True(pattern.IsMatch("https://example.com/books/123"));
Assert.True(pattern.IsMatch("http://localhost:5000/books/123"));
Assert.False(pattern.IsMatch("https://example.com/authors/123"));

The delimiters are optional: the : after the protocol and the ? and # before the search and the hash are removed, so Search = "?page=1" and Search = "page=1" are the same pattern. Set BaseUrl to take the components that a relative pattern does not give from a URL; the text of the base URL is literal, so a : or a * in it is not pattern syntax.

The two forms are equivalent: prefer the string for a pattern that reads like a URL, and UrlPatternInit when only a few components matter, or when a component comes from configuration.

Match a URL

IsMatch tells whether a URL matches. It accepts a string, a relative URL with its base URL, a Uri, or the components of a URL in a UrlPatternInit:

var pattern = UrlPattern.Create("https://example.com/books/:id");

// A string
Assert.True(pattern.IsMatch("https://example.com/books/123"));

// A relative URL and the base URL to resolve it against
Assert.True(pattern.IsMatch("/books/123", "https://example.com"));

// A System.Uri. A relative Uri never matches.
Assert.True(pattern.IsMatch(new System.Uri("https://example.com/books/123")));
Assert.False(pattern.IsMatch(new System.Uri("/books/123", UriKind.Relative)));

// The components of a URL, given separately
Assert.True(pattern.IsMatch(new UrlPatternInit
{
    Protocol = "https",
    Hostname = "example.com",
    Pathname = "/books/123",
}));

// A string that is not a URL does not match, it does not throw
Assert.False(pattern.IsMatch("not a url"));

A string that cannot be parsed as a URL does not match: IsMatch returns false and Match returns null, so you can pass untrusted input without catching exceptions. A relative Uri never matches; resolve it first, or pass the relative string with its base URL.

Read the values of the groups

Match returns a UrlPatternResult, or null when the URL does not match. The result has one UrlPatternComponentResult per component, with the value that was matched (Input) and the value of each group (Groups):

var pattern = UrlPattern.Create("https://:tenant.example.com/orders/:orderId");

var result = pattern.Match("https://contoso.example.com/orders/42?page=2");
Assert.NotNull(result);

// One result per component: the value it was matched against and its groups
Assert.Equal("contoso", result.Hostname.Groups["tenant"]);
Assert.Equal("42", result.Pathname.Groups["orderId"]);
Assert.Equal("/orders/42", result.Pathname.Input);
Assert.Equal("page=2", result.Search.Input);

// The URL that was matched, as given
Assert.Equal("https://contoso.example.com/orders/42?page=2", result.Inputs[0].Url);

// No match: Match returns null
Assert.Null(pattern.Match("https://contoso.example.com/invoices/42"));
  • Groups is keyed by the name of the group. A group without a name, such as a wildcard, is keyed by its index: "0", "1"… See Pattern syntax.
  • The value of an optional group that matched nothing is null.
  • Input is the component of the URL as it was compared, without its delimiter: page=2, not ?page=2.
  • Inputs holds what you passed to Match: Url for a string or a Uri, Init for a UrlPatternInit.

Ignore the case

By default, the pathname, the search and the hash are case-sensitive. Set UrlPatternOptions.IgnoreCase to compare them case-insensitively:

var caseSensitive = UrlPattern.Create(new UrlPatternInit { Pathname = "/Books/:id" });
Assert.False(caseSensitive.IsMatch("https://example.com/books/123"));

// IgnoreCase applies to the pathname, the search and the hash
var options = new UrlPatternOptions { IgnoreCase = true };
var pattern = UrlPattern.Create(new UrlPatternInit { Pathname = "/Books/:id", Search = "lang=en" }, options);
Assert.True(pattern.IsMatch("https://example.com/BOOKS/123?LANG=EN"));

// The protocol and the hostname are always compared in lower case
var hostPattern = UrlPattern.Create("https://example.com/*");
Assert.True(hostPattern.IsMatch("HTTPS://EXAMPLE.COM/books"));

The option does not apply to the other components. The protocol and the hostname are compared in lower case whatever the option; the username, the password and the port are always case-sensitive.

Handle invalid patterns

UrlPattern.Create throws a UrlPatternException when the pattern cannot be parsed: an unclosed group, an invalid regular expression, a protocol or a hostname that is not valid, a port out of range, or a pattern string without a protocol and without a base URL:

// An unclosed regular expression group
Assert.Throws<UrlPatternException>(() => UrlPattern.Create(new UrlPatternInit { Pathname = "/books/:id(" }));

// A pattern string without a protocol, and no base URL
Assert.Throws<UrlPatternException>(() => UrlPattern.Create("/books/:id"));

Create patterns once, at startup or when the configuration loads, and reuse them: parsing a pattern builds its regular expressions, while matching only runs them.

Next steps