Getting Started
ASP.NET Core is heavyweight and does not run on .NET MAUI or in several embedded server scenarios. This is a dependency-light, AOT- and trim-clean HTTP/1.1, HTTP/2 and HTTP/3 server that runs anywhere .NET runs — plus tunnelling, so a server embedded in a phone app is reachable from the public internet.
Only Microsoft.Extensions.* abstractions are taken as dependencies. Everything else — JSON, crypto,
JWT, OpenAPI, HPACK, QPACK — is built on what is in the box.
| GitHub | |
| Downloads |
Packages
Section titled “Packages”Everything shipping targets net10.0 and has the trim, AOT and single-file analyzers turned on, so
“AOT-clean” is enforced by the build rather than claimed in a readme.
Install
Section titled “Install”dotnet add package Shiny.Net.HttpServer
# optional, and what tier 3 below needsdotnet add package Shiny.Net.HttpServer.SourceGeneratorsThe generator package is an analyzer — it runs inside the compiler and never lands in your output.
A server in three lines
Section titled “A server in three lines”using Shiny.Net.HttpServer;
var server = new HttpServer(new HttpServerOptions { Port = 8080 });server.OnGet("/ping", ctx => ctx.Response.WriteAsync("pong"));await server.RunAsync();That is the whole ceremony. No container, no host builder, no configuration file. The server binds loopback by default — a server embedded in a mobile app should not be reachable from the local network unless its author says so.
The four tiers
Section titled “The four tiers”The point of the API is a gentle ramp: trivial to start, strongly typed when you want it. Each tier is built on the one below and they compose in the same app.
-
Tier 0 — one delegate, no routing.
server.OnRequest(ctx => ctx.Response.WriteAsync("hello")); -
Tier 1 — raw routing.
OnGet/OnPost/… (orMapGet/MapPost/…, the ASP.NET spelling).server.OnGet("/ping", ctx => ctx.Response.WriteAsync("pong"));server.OnGet("/users/{id:int}", ctx => ctx.Response.WriteAsync(ctx.Request.RouteValues["id"]!));See Routing.
-
Tier 2 — middleware. The same shape as ASP.NET Core middleware, as a lambda or as a class.
server.Use(async (ctx, next) =>{var sw = Stopwatch.StartNew();await next(ctx);logger.LogInformation("{Path} took {Elapsed}ms", ctx.Request.Path, sw.ElapsedMilliseconds);});See Middleware.
-
Tier 3 — source-generated typed endpoints. Route registration, parameter binding and OpenAPI metadata are emitted at compile time — no reflection, which is what keeps the whole thing trim- and AOT-clean.
[Route("/api/users")]public class UserEndpoints(IUserService users, ILogger<UserEndpoints> logger){[Get("/{id:int}")]public async Task<IActionResult> GetUser(int id, CancellationToken ct)=> await users.FindAsync(id, ct) is { } u ? new OkObjectResult(u) : new NotFoundResult();}app.MapMyAppEndpoints(); // emitted for every [Route] class in the assemblySee Typed Endpoints.
Dependency injection is available, never mandatory
Section titled “Dependency injection is available, never mandatory”// No container. RequestServices resolves nothing; everything else works.var server = new HttpServer(new HttpServerOptions { Port = 8080 });
// With a container. Scoped services behave exactly as in ASP.NET Core.var builder = HttpServer.CreateBuilder();builder.Services.AddSingleton<IClock, SystemClock>();builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
var app = builder.Build();A real IServiceScope is created per request/response exchange and disposed when the request ends,
including IAsyncDisposable. Endpoint classes are resolved from that scope, so a Scoped dependency
is one instance shared by everything handling the request — the same contract as ASP.NET Core.
ctx.RequestServices is the accessor at every tier.
If the app already has a container — a MAUI app, a generic host — use AddHttpServer() instead. See
Hosting & Lifecycle.
Where to go next
Section titled “Where to go next”| If you want to… | Read |
|---|---|
| Start, stop and restart the server at runtime | Hosting & Lifecycle |
| Bind several ports, set limits, turn on forwarded headers | Configuration |
| Understand templates, constraints and match precedence | Routing |
| Return JSON without reflection | Results & JSON |
| Serve a web app out of the assembly | Static Files or Blazor WebAssembly |
| Authenticate callers | Authentication, JWT |
| Reach the device from the internet | Tunnelling |
| Run this inside a .NET MAUI app | .NET MAUI |
| Host an MCP server | Model Context Protocol |
AOT and trimming
Section titled “AOT and trimming”This is the constraint the whole design answers to, so it is worth stating plainly: nothing in the
server discovers anything by reflection. Routes are registered by generated code, parameters are
bound by generated code, and JSON goes through JsonTypeInfo from a JsonSerializerContext rather
than through Type.GetProperties().
The one thing you have to bring is that context:
[JsonSerializable(typeof(Widget))][JsonSerializable(typeof(IReadOnlyList<Widget>))]public partial class AppJson : JsonSerializerContext;The endpoint generator emits a module initializer that registers it for you, and warns at build time (SWS006) about any type crossing an endpoint boundary that the context does not cover — turning a runtime failure into a build warning. See Results & JSON.
The reflection-based JSON overloads still exist for apps that are not published trimmed, but they are
annotated [RequiresUnreferencedCode]/[RequiresDynamicCode], so an AOT build has to opt into them
deliberately.


