NanoRoute 1.0.0-preview2

This is a prerelease version of NanoRoute.
dotnet add package NanoRoute --version 1.0.0-preview2
                    
NuGet\Install-Package NanoRoute -Version 1.0.0-preview2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NanoRoute" Version="1.0.0-preview2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NanoRoute" Version="1.0.0-preview2" />
                    
Directory.Packages.props
<PackageReference Include="NanoRoute" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NanoRoute --version 1.0.0-preview2
                    
#r "nuget: NanoRoute, 1.0.0-preview2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NanoRoute@1.0.0-preview2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NanoRoute&version=1.0.0-preview2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=NanoRoute&version=1.0.0-preview2&prerelease
                    
Install as a Cake Tool

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 to RequestContext.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.MatchingPrecedence decides 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:

  • int supports min and max.
  • str supports min, max, and pattern.
  • guid and bool do 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.Parameters under 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.Parameters by default, using the property name as the key.
  • RequestContext properties receive the current request context automatically.
  • CancellationToken properties 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 from RequestContext.Services.
  • [ValueSource(ValueSource.ServiceLocator, Name = "...")] resolves a keyed service.
  • [ValueSource(ValueSource.Skip)] leaves the property untouched and does not allow Name.
  • 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.Cancellation and RequestContext.Cancellation.
  • OperationCanceledException is not converted into an HTTP error by AddExceptionHandler() or AddJsonErrorDetails(). It propagates to the caller or transport adapter unchanged.
  • HttpListenerRouter.Route() aborts the active HttpListenerResponse and then rethrows the cancellation exception.

Common Building Blocks

  • HttpListenerRouter.CreateBuilder() starts a strongly typed builder for HttpListener scenarios.
  • AddDefaultValueParsers() registers the built-in int, guid, bool, and str value 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 into RequestContext.Parameters.
  • AddHandler<TRequestContext>() projects RequestContext into a typed request object before invoking the handler.
  • AddJsonBody() binds JSON request content into RequestContext.Parameters.
  • AddJsonErrorDetails() turns routing exceptions into JSON ErrorDetails responses.
  • HttpResponseMessage.Json(...) creates JSON responses with the library's serializer defaults.

Core Types

Documentation

API documentation is generated from the XML comments in the source and published at:

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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