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| Package | Description |
|---|---|
| The server: HTTP/1.1, HTTP/2 & HTTP/3, routing, middleware, DI scopes, static files, WebSockets, SSE, sessions, OpenAPI, CORS, rate limiting, IP filtering, tunnelling. Includes the typed-endpoint source generator | |
JWT authentication on in-box crypto no Microsoft.IdentityModel dependency |
|
| Azure Relay tunnel provider | |
| SSH remote-forwarding tunnel provider, including zero-account quick tunnels | |
| cloudflared, ngrok and Tailscale agents, supervised. Desktop and CLI, not mobile | |
| Model Context Protocol (Streamable HTTP) transport host an MCP server without ASP.NET Core, including inside a MAUI app | |
| Shiny.Mediator requests, commands and streams published as endpoints generated at compile time. Generator included | |
| Shiny.DocumentDb types as REST resources list, by-id, count, CRUD, merge-patch and a live SSE tail | |
| WebDAV (RFC 4918) class 1 & 2 over a directory mount an apps storage in Finder or Explorer, and open the same URL in a browser for a file manager | |
| gRPC and gRPC-Web unary, streaming and bidirectional methods over the same HTTP/2 stack, with serialization you supply | |
| mDNS/Bonjour advertise the server on the local link, and find the ones other devices advertise | |
| Mobile lifecycle background and resume, an Android foreground service, rebinding when the device moves | |
An in-memory HttpClient endpoint tests with no port and no listener |
|
A .NET tool shinyhttpserver that serves a directory over WebDAV: a browser file manager and a mountable drive at one address, with a QR code in the banner |
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- MyApp/
- Platforms
- iOS
- Info.plist
- iOS
- MauiProgram.cs
- MyApp.csproj
- Platforms
1<?xml version="1.0" encoding="UTF-8"?>2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">3<plist version="1.0">4<dict>5 <key>ITSAppUsesNonExemptEncryption</key>6 <false/>7 <key>UIDeviceFamily</key>8 <array>9 <integer>1</integer>10 <integer>2</integer>11 </array>12 <key>UIRequiredDeviceCapabilities</key>13 <array>14 <string>arm64</string>15 </array>16 <key>UISupportedInterfaceOrientations</key>17 <array>18 <string>UIInterfaceOrientationPortrait</string>19 <string>UIInterfaceOrientationLandscapeLeft</string>20 <string>UIInterfaceOrientationLandscapeRight</string>21 </array>22 <key>UISupportedInterfaceOrientations~ipad</key>23 <array>24 <string>UIInterfaceOrientationPortrait</string>25 <string>UIInterfaceOrientationPortraitUpsideDown</string>26 <string>UIInterfaceOrientationLandscapeLeft</string>27 <string>UIInterfaceOrientationLandscapeRight</string>28 </array>29 <key>XSAppIconAssets</key>30 <string>Assets.xcassets/appicon.appiconset</string> 31 <!-- iOS 14+ gates anything touching the local network, and that includes SERVING on it.32 Without this key the app is denied without ever being asked. -->33 <key>NSLocalNetworkUsageDescription</key>34 <string>Say something useful here that your users will understand</string>35</dict>36</plist>dotnet add package Shiny.Net.HttpServerThat one package is also what tier 3 below needs: the typed-endpoint generator ships inside it under
analyzers/, so there is no second reference to add. It runs inside the compiler and never lands in
your output.
In the test project, add the in-memory harness it is the only other package most apps ever need:
dotnet add package Shiny.Net.HttpServer.TestingA server in three lines
Section titled A server in three linesusing Shiny.Net.HttpServer;
var server = new HttpServer(new HttpServerOptions { Port = 8080 });server.MapGet("/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 tiersThe 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.
MapGet/MapPost/, the ASP.NET Core spelling.server.MapGet("/ping", ctx => ctx.Response.WriteAsync("pong"));server.MapGet("/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 AddShinyHttpServer() instead. See
Hosting & Lifecycle.
Testing an endpoint
Section titled Testing an endpointShiny.Net.HttpServer.Testing wires an HttpClient straight to the server through a pair of pipes,
so a test costs no port, binds no listener and leaves nothing behind when it fails half way through:
await using var app = TestHttpServer.Create(server => server.MapGet("/ping", ctx => ctx.Response.WriteTextAsync("pong")));
Assert.Equal("pong", await app.Client.GetStringAsync("/ping"));Only the socket is replaced the request still goes through the real parser, router, middleware and
response framing. TestHttpServer.Create takes the same builder the app uses, so substituting a
dependency in a test is the ordinary builder.Services.AddSingleton(...). See
Testing.
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 or mobile app | Mobile |
| Host an MCP server | Model Context Protocol |
| Publish Shiny.Mediator handlers or a DocumentDb type | Shiny.Mediator, Shiny.DocumentDb |
| Test endpoints without a port | Testing |
AOT and trimming
Section titled AOT and trimmingThis 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.