UrlMatcher 3.1.0
dotnet add package UrlMatcher --version 3.1.0
NuGet\Install-Package UrlMatcher -Version 3.1.0
<PackageReference Include="UrlMatcher" Version="3.1.0" />
<PackageVersion Include="UrlMatcher" Version="3.1.0" />
<PackageReference Include="UrlMatcher" />
paket add UrlMatcher --version 3.1.0
#r "nuget: UrlMatcher, 3.1.0"
#:package UrlMatcher@3.1.0
#addin nuget:?package=UrlMatcher&version=3.1.0
#tool nuget:?package=UrlMatcher&version=3.1.0
UrlMatcher
Simple URL matcher library allowing you to match based on explicit strings, parameters, and catch-all segments, targeted to .NET Standard 2.0/2.1, .NET Framework 4.6.2/4.8, and .NET 8.0/10.0.
Help or Feedback
First things first - do you need help or have feedback? File an issue here! We'd love to hear from you.
New in v3.1.0
- Catch-all segments:
/api/{*rest}matches/api,/api/users, and/api/users/42/orders UrlPatternparses a pattern once for reuse, reports its shape for route ranking, and rejects invalid patterns up front- A failed match always returns an empty collection (3.0.2 could leave values captured before the failing segment)
See CHANGELOG.md for compatibility notes.
Install
dotnet add package UrlMatcher
Usage
Static Method
Use the static method when you need to compare an input URL against a pattern once.
using System.Collections.Specialized;
using UrlMatcher;
if (Matcher.Match("/v1.0/users/42", "/{version}/users/{userId}", out NameValueCollection vals))
{
Console.WriteLine("version : " + vals["version"]); // v1.0
Console.WriteLine("userId : " + vals["userId"]); // 42
}
Instance Method
Instantiate the class with a URL or Uri to parse it once, then match it against many patterns.
Matcher matcher = new Matcher("/v1.0/users/42");
if (matcher.Match("/{version}/users/{userId}", out NameValueCollection vals))
{
// vals["userId"] == "42"
}
else if (matcher.Match("/{version}/hobbies/{hobbyId}", out vals))
{
// ...
}
Catch-All Segments
A catch-all is written {*name} and must be the entire final segment of the pattern. It matches zero or more remaining URL segments and captures them as the raw remainder of the URL.
Matcher.Match("/api/users/42/orders", "/api/{*rest}", out NameValueCollection vals); // true, rest = "users/42/orders"
Matcher.Match("/api", "/api/{*rest}", out vals); // true, rest = ""
Matcher.Match("/v1/files/a/b.txt", "/{v}/files/{*path}", out vals); // true, v = "v1", path = "a/b.txt"
Matcher.Match("/other/users", "/api/{*rest}", out vals); // false
The captured value is the remainder of the URL exactly as supplied (after the query string and fragment are removed), so it is safe to substitute into a forwarded path:
| URL | Pattern | rest |
|---|---|---|
/api/a//b |
/api/{*rest} |
a//b (repeated slashes kept) |
/api/a/b/ |
/api/{*rest} |
a/b/ (trailing slash kept) |
/api/a%2Fb/c |
/api/{*rest} |
a%2Fb/c (not decoded) |
/api/a/b?x=1#f |
/api/{*rest} |
a/b (query and fragment removed) |
A catch-all that is not the last segment, appears more than once, or shares its segment with other text (for example /{*rest}/edit, /{*a}/{*b}, or /files/v{*x}) throws ArgumentException.
Pre-Parsed Patterns
UrlPattern parses a pattern once. Routers should parse patterns when routes are registered, so an invalid pattern fails immediately instead of on every request, and matching skips re-parsing.
UrlPattern pattern = UrlPattern.Parse("/{v}/files/{*path}"); // throws ArgumentException if invalid
if (Matcher.Match("/v1/files/a/b.txt", pattern, out NameValueCollection vals)) { ... }
if (matcher.Match(pattern, out vals)) { ... }
if (!UrlPattern.TryParse(userSupplied, out UrlPattern parsed)) { /* reject the route */ }
UrlPattern also reports facts a router can use to rank routes: SegmentCount, FixedSegmentCount, LiteralCount, ParameterCount, LiteralPrefixCount, IsCatchAll, CatchAllName, and Segments (each with Text, Type, and Name). The ranking policy itself is left to the router.
Matching Rules
- URLs and patterns are split on
/and empty segments are discarded, so leading, trailing, and repeated slashes do not matter (except inside a catch-all value). - The query string and fragment are removed from the URL before matching. They are not removed from the pattern.
- The URL and pattern must have the same number of segments, unless the pattern ends in a catch-all.
- Literal segments are compared ordinally (case-sensitive, no culture or Unicode normalization).
- Parameter names are case-insensitive. Captured values keep their case and are not URL-decoded.
- A failed match returns
falseand an empty (never null) collection. - Duplicate parameter names are joined with a comma by
NameValueCollection(/{id}/{id}against/a/bgivesid=a,b).
Pattern Syntax
| Form | Example pattern | Matches (captured values) | Does not match |
|---|---|---|---|
{name} |
/users/{id} |
/users/42 (id=42) |
/users, /users/42/orders |
{*name} as the whole last segment |
/api/{*rest} |
/api/users/42 (rest=users/42), /api (rest="") |
/other/users, /API/users |
{*name} not last |
/{*rest}/edit |
throws ArgumentException |
|
More than one {*name} |
/{*a}/{*b} |
throws ArgumentException |
|
{*name} with other text in the segment |
/files/v{*x} |
throws ArgumentException |
|
{*} |
/files/{*} |
/files/{*} (literal) |
/files/42 |
{} |
/files/{} |
/files/{} (literal) |
/files/42 |
* or ** |
/files/* |
/files/* (literal) |
/files/a, /files/a/b |
| Unclosed brace | /users/{id |
/users/{id (literal) |
/users/42 |
| Literal | /Users/list |
/Users/list |
/users/list |
Some brace forms are kept for compatibility with earlier versions and are best avoided:
| Form | Example pattern | Behavior |
|---|---|---|
| Braces inside a segment | /v{version}/users |
/v1/users gives version=v1 (the whole segment, not 1) |
| Two groups in one segment | /item/{a}{b} |
/item/xy gives a=xy; b is never set |
| Spaces in a name | /users/{ id } |
The key is id, so vals["id"] is null |
| Query text in a pattern | /search?q={q} |
One segment; /search?q=1 gives q=search |
Thread Safety
Matcher, UrlPattern, and UrlPatternSegment instances are immutable after construction and safe to use from multiple threads concurrently. Each call to Match returns a new collection.
Testing
Tests use Touchstone. Test descriptors live in src/Test.Shared and run through three runners:
dotnet run --project src/Test.Automated -- --results results.json
dotnet test src/Test.Xunit
dotnet test src/Test.Nunit
Version History
Please refer to CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- No dependencies.
-
.NETFramework 4.8
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
-
.NETStandard 2.1
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on UrlMatcher:
| Package | Downloads |
|---|---|
|
Watson
Simple, fast, async C# web server for handling REST requests with unified 7.x protocol architecture targeting .NET Standard 2.1, .NET 8, and .NET 10. |
|
|
Watson.Core
Core library for Watson 7.x shared semantics, routing, lifecycle, and protocol configuration targeting .NET 8 and .NET 10. |
|
|
HttpServerLite
User-space HTTP and HTTPS server without http.sys written in C#. Please visit the project site README for details on implementation. |
|
|
OllamaFlow.Core
OllamaFlow is a lightweight intelligent load-balancer for Ollama. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on UrlMatcher:
| Repository | Stars |
|---|---|
|
dotnet/WatsonWebserver
Watson is the fastest, easiest way to build scalable RESTful web servers and services in C#.
|
Catch-all segments ({*name}), UrlPattern for pre-parsed patterns, failed matches always return an empty collection.