NanoRoute 1.0.0-preview2
dotnet add package NanoRoute --version 1.0.0-preview2
NuGet\Install-Package NanoRoute -Version 1.0.0-preview2
<PackageReference Include="NanoRoute" Version="1.0.0-preview2" />
<PackageVersion Include="NanoRoute" Version="1.0.0-preview2" />
<PackageReference Include="NanoRoute" />
paket add NanoRoute --version 1.0.0-preview2
#r "nuget: NanoRoute, 1.0.0-preview2"
#:package NanoRoute@1.0.0-preview2
#addin nuget:?package=NanoRoute&version=1.0.0-preview2&prerelease
#tool nuget:?package=NanoRoute&version=1.0.0-preview2&prerelease
NanoRoute
NanoRoute is a small, dependency-light router for HttpRequestMessage pipelines, with optional transport adapters and focused helpers for JSON payloads and error handling.
The core library is centered around RouteBuilder, Router, and RequestContext, so you can plug the routing pipeline into your own transport or hosting model as well.
NanoRoute targets netstandard2.0 and netstandard2.1.
Note: NanoRoute is compatible with Native AOT scenarios.
For AWS Lambda integrations, use the separate NanoRoute.AwsLambda package.
Quick Start
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NanoRoute;
using NanoRoute.Json;
HttpListenerRouter router = HttpListenerRouter
.CreateBuilder()
.AddValueParser("int", static (ReadOnlyMemory<char> segment, object? _, out object? parsed) =>
{
bool success = int.TryParse(segment.Span, out int value);
parsed = success ? value : null;
return success;
})
// Convert routing exceptions into JSON error responses.
.AddJsonErrorDetails()
.AddHandler("GET", "/api/users/{user_id:int}/", async (context, next) =>
{
context.Parameters["user"] = $"user-{context.Parameters["user_id"]}";
return await next();
})
.AddHandler("GET", "/api/users/{user_id:int}/details", async (context, _) =>
{
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent((string) context.Parameters["user"]!)
};
})
.CreateRouter();
HttpListener listener = new();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();
HttpListenerContext context = await listener.GetContextAsync();
await router.Route(context, new ServiceCollection().BuildServiceProvider());
In this example, /api/users/{user_id:int}/ is a prefix route, so it runs before the more specific /api/users/{user_id:int}/details handler and can populate shared state in RequestContext.Parameters. HttpListenerRouter converts the incoming HttpListenerContext into an HttpRequestMessage, executes the NanoRoute pipeline, and copies the produced HttpResponseMessage back to the listener response.
Matching Rules
- A trailing
/makes a route a prefix match. - Without a trailing
/, the route matches only the exact normalized request path. - Route patterns must start with
/, except for the empty string"", which matches the current scoped path exactly. - Repeated
/separators in route patterns, such as//or/items//details, are invalid. - Literal segments are matched case-insensitively.
- Parser-backed segments use registered parsers such as
{user_id:int},{int}, or{slug:str(min=3,max=32)}. - The parameter name is optional. Segments like
{int}still validate the path but do not add an entry toRequestContext.Parameters. - When multiple handlers match within the selected route branch, NanoRoute evaluates compatible handlers from shorter prefixes toward more specific matches.
- At the same path depth,
RouterConfig.MatchingPrecedencedecides whether literal or parameterized child segments are selected first. - Once NanoRoute selects a child branch at a given depth, it does not return to sibling branches later in the pipeline.
Advanced Usage
AddPrefix() and CreatePrefix()
When several routes share the same prefix, AddPrefix() lets you define that prefix once and register child routes relative to it. If you want to hold onto a scoped child builder and add routes incrementally, use CreatePrefix().
RouterBuilder<HttpListenerRouter, HttpListenerRouterConfig> builder = HttpListenerRouter
.CreateBuilder()
// Register the built-in int/guid/bool/str route parsers.
.AddDefaultValueParsers()
// Convert routing exceptions into JSON error responses.
.AddJsonErrorDetails();
builder.AddPrefix("/api/users/{user_id:int}/", users => users
.AddHandler("GET", "/", async (context, next) =>
{
context.Parameters["user"] = $"user-{context.Parameters["user_id"]}";
return await next();
})
.AddHandler("GET", "/details", async (context, _) =>
{
return HttpResponseMessage.Json(new
{
id = context.Parameters["user_id"],
name = context.Parameters["user"]
});
}));
HttpListenerRouter router = builder.CreateRouter();
This produces the same effective routes as registering /api/users/{user_id:int}/ and /api/users/{user_id:int}/details directly, but keeps repeated base patterns out of individual AddHandler() calls.
Value Parsers
NanoRoute supports both synchronous and asynchronous value parsers:
AddValueParser("name", SyncValueParserDelegate)for lightweight synchronous parsing.AddValueParser("name", ValueParserDelegate)when parsing needs request services or async work.AddValueParser("name", BindArgumentsDelegate, ...)when the route template includes parser arguments such as{id:int(min=1)}.
BindArgumentsDelegate receives the raw parser arguments as a case-insensitive dictionary and can turn them into any cached object. That object is then exposed as ValueParserContext.Arguments for async parsers or as the arguments parameter for sync parsers.
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using NanoRoute;
RouterBuilder<HttpListenerRouter, HttpListenerRouterConfig> builder = HttpListenerRouter
.CreateBuilder()
.AddValueParser
(
"int",
static (IReadOnlyDictionary<string, string> rawArgs) => (
Min: rawArgs.TryGetValue("min", out string? min) ? int.Parse(min, CultureInfo.InvariantCulture) : null,
Max: rawArgs.TryGetValue("max", out string? max) ? int.Parse(max, CultureInfo.InvariantCulture) : null
),
static (ReadOnlyMemory<char> segment, object? arguments, out object? parsed) =>
{
(int? Min, int? Max) limits = ((int? Min, int? Max)) arguments!;
parsed = null;
if (!int.TryParse(segment.Span, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
return false;
if (limits.Min is int min && value < min)
return false;
if (limits.Max is int max && value > max)
return false;
parsed = value;
return true;
}
)
.AddValueParser("user", static async (ValueParserContext context) =>
{
if (!Guid.TryParse(context.Segment.Span, out Guid userId))
return new ValueParseResult(false, null);
IUserRepository repository = context.Services.GetRequiredService<IUserRepository>();
object? user = await repository.TryGetAsync(userId, context.Cancellation);
return new ValueParseResult(user is not null, user);
});
Built-in parsers use the same mechanism:
intsupportsminandmax.strsupportsmin,max, andpattern.guidandbooldo not take arguments.
Query Bindings
AddQueryBindings() lets you validate and parse selected query-string values with the same registered value parsers used by route segments.
HttpListenerRouter router = HttpListenerRouter
.CreateBuilder()
.AddDefaultValueParsers()
.AddPrefix("/items/", items => items
.AddQueryBindings("GET", "", "{filter:str(min=3)}&{page?:int(min=1)}")
.AddHandler("GET", "", async (context, _) =>
{
return HttpResponseMessage.Json(new
{
filter = context.Parameters["filter"],
page = context.Parameters.TryGetValue("page", out object? page) ? page : null
});
}))
.CreateRouter();
- Add
?to the query parameter name to make it optional, for example{page?:int(min=1)}. - Query parameter names may contain ASCII letters, digits, and underscores.
- Parsed values are stored in
RequestContext.Parametersunder the configured key. - Query keys are matched case-insensitively using the normalized key exposed by
Uri.Query. - Repeated declared query parameters are rejected with
400 Bad Request. - As with JSON binding and prefix handlers, later middleware can overwrite earlier values in
RequestContext.Parameters.
Typed Handlers
Typed handlers let you describe the data a route needs as a request object instead of reading everything manually from RequestContext.Parameters.
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NanoRoute;
using NanoRoute.HandlerExtensions;
public sealed class GetItemRequest
{
public int Id { get; set; }
[ValueSource(ValueSource.Context, Name = "query_filter")]
public string Filter { get; set; } = null!;
[ValueSource(ValueSource.ServiceLocator)]
public IItemService Items { get; set; } = null!;
[ValueSource(ValueSource.Skip)]
public string? DiagnosticsLabel { get; set; }
public CancellationToken Cancellation { get; set; }
}
HttpListenerRouter router = HttpListenerRouter
.CreateBuilder()
.AddDefaultValueParsers()
.AddHandler
(
["GET"],
"/items/{id:int}",
"{query_filter:str(min=3)}",
async (GetItemRequest request) =>
{
Item item = await request.Items.GetAsync(request.Id, request.Filter, request.Cancellation);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(item.Name)
};
}
)
.CreateRouter();
Binding rules:
- Writable public properties are bound from
RequestContext.Parametersby default, using the property name as the key. RequestContextproperties receive the current request context automatically.CancellationTokenproperties receive the active request token automatically.[ValueSource(ValueSource.Context, Name = "...")]binds from a different parameter or query-binding name.[ValueSource(ValueSource.ServiceLocator)]resolves a service fromRequestContext.Services.[ValueSource(ValueSource.ServiceLocator, Name = "...")]resolves a keyed service.[ValueSource(ValueSource.Skip)]leaves the property untouched and does not allowName.- Read-only properties are ignored.
- Missing required values or services fail fast with
InvalidOperationException.
Typed handlers also have middleware-style overloads that receive CallNextHandlerDelegate:
.AddHandler
(
["GET"],
"/items/{id:int}",
async (GetItemRequest request, CallNextHandlerDelegate next) =>
{
HttpResponseMessage response = await next();
response.Headers.Add("X-Filter", request.Filter);
return response;
}
)
Custom Routers
If HttpListenerRouter is not the transport you want, you can derive from Router and expose your own entry point that prepares an HttpRequestMessage, invokes Handle(), and deals with the returned HttpResponseMessage.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NanoRoute;
public sealed class InMemoryRouter(RouterBuilder<InMemoryRouter, RouterConfig> builder) : Router(builder, builder.RouterConfig)
{
public static RouterBuilder<InMemoryRouter, RouterConfig> CreateBuilder() =>
new(static builder => new InMemoryRouter(builder));
public Task<HttpResponseMessage> Route(HttpRequestMessage request, IServiceProvider services, CancellationToken cancellation = default) =>
Handle(request, services, cancellation);
}
InMemoryRouter router = InMemoryRouter
.CreateBuilder()
.AddHandler("GET", "/health", async (_, _) => new HttpResponseMessage())
.CreateRouter();
This keeps the transport-specific concerns in your own router type while still reusing NanoRoute's matching, value parsing, and handler pipeline.
Cancellation
- NanoRoute exposes the caller-provided cancellation token to async value parsers and handlers through
ValueParserContext.CancellationandRequestContext.Cancellation. OperationCanceledExceptionis not converted into an HTTP error byAddExceptionHandler()orAddJsonErrorDetails(). It propagates to the caller or transport adapter unchanged.HttpListenerRouter.Route()aborts the activeHttpListenerResponseand then rethrows the cancellation exception.
Common Building Blocks
HttpListenerRouter.CreateBuilder()starts a strongly typed builder forHttpListenerscenarios.AddDefaultValueParsers()registers the built-inint,guid,bool, andstrvalue parsers.AddPrefix("/prefix/", ...)configures a scoped route subtree and returns the current builder.CreatePrefix("/prefix/")creates a scoped child builder for a route subtree.AddQueryBindings()binds selected query-string values intoRequestContext.Parameters.AddHandler<TRequestContext>()projectsRequestContextinto a typed request object before invoking the handler.AddJsonBody()binds JSON request content intoRequestContext.Parameters.AddJsonErrorDetails()turns routing exceptions into JSONErrorDetailsresponses.HttpResponseMessage.Json(...)creates JSON responses with the library's serializer defaults.
Core Types
- RouteBuilder
- Router
- RouterBuilder`2
- HttpListenerRouter
- RequestContext
- ErrorDetails
- ValueParserDelegate
- RequestHandlerDelegate
- NanoRouteHandlerExtensions
- ValueSource
- ValueSourceAttribute
Documentation
API documentation is generated from the XML comments in the source and published at:
| 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 was computed. 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 was computed. 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 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. 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. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- System.Collections.Immutable (>= 10.0.7)
- System.Memory (>= 4.6.3)
- System.Text.Json (>= 10.0.7)
-
.NETStandard 2.1
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- System.Collections.Immutable (>= 10.0.7)
- System.Text.Json (>= 10.0.7)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NanoRoute:
| Package | Downloads |
|---|---|
|
NanoRoute.AwsLambda
AWS Lambda adapters for NanoRoute. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview2 | 33 | 5/6/2026 |
| 1.0.0-preview1 | 41 | 4/30/2026 |