ASP.NET Core is heavyweight and does not run on .NET MAUI or in several embedded server scenarios. This is a dependency-light, fully AOT/trim-clean HTTP/1.1, HTTP/2 & 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.
| Package | Description |
|---|---|
| Shiny.Net.HttpServer | 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 |
| Shiny.Net.HttpServer.Jwt | JWT authentication on in-box crypto — no Microsoft.IdentityModel dependency |
| Shiny.Net.HttpServer.AzureRelay | Azure Relay tunnel provider |
| Shiny.Net.HttpServer.Ssh | SSH remote-forwarding tunnel provider, including zero-account quick tunnels |
| Shiny.Net.HttpServer.Mcp | Model Context Protocol (Streamable HTTP) transport — host an MCP server without ASP.NET Core, including inside a MAUI app |
| Shiny.Net.HttpServer.Mediator | Publishes Shiny.Mediator requests, commands and streams as endpoints generated at compile time. Generator included |
| Shiny.Net.HttpServer.DocumentDb | Publishes a Shiny.DocumentDb type as a REST resource — list, by-id, count, CRUD, merge-patch and a live SSE tail |
| Shiny.Net.HttpServer.WebDav | A WebDAV (RFC 4918) class 1 & 2 server over a directory — mount an app's storage in Finder, Windows Explorer or any WebDAV client, and open the same URL in a browser for a file manager with upload, rename and delete |
| Shiny.Net.HttpServer.Grpc | gRPC and gRPC-Web — unary, streaming and bidirectional methods over the same HTTP/2 stack, with serialization you supply |
| Shiny.Net.HttpServer.Discovery | mDNS/DNS-SD (Bonjour) — advertises the server on the local link and finds the ones other devices advertise, so nobody has to type an IP address |
| Shiny.Net.HttpServer.Mobile | Mobile lifecycle on Shiny.Core — stop on background and start on resume, an Android foreground service that follows the server's own running state to keep serving, an iOS resume that restarts the server the suspension took, rebind when the device changes network, and a check for the manifest entries that silently break local networking. iOS and Android, with or without MAUI |
| Shiny.Net.HttpServer.Testing | An HttpClient wired to the server through memory — endpoint tests with no port, no listener and no socket, but the real parser, router and middleware |
| Shiny.Net.HttpServer.Tunnels | Agent-backed tunnels — supervises cloudflared, ngrok or tailscale and reports the public URL. Desktop, server and CLI; on a phone use the SSH provider or the relay |
| Shiny.Net.HttpServer.CommandLine | A .NET tool — shinyhttpserver — that serves a directory over WebDAV, so one address is both a browser file manager (browse, upload, rename, delete) and a drive Finder or Explorer can mount, with basic auth, per-operation permissions, and a QR code in the banner so a phone can scan its way in — --tunnel swaps the LAN address for a public pinggy.io tunnel so the phone need not be on the same network |
var server = new HttpServer(new HttpServerOptions { Port = 8080 });
server.MapGet("/ping", ctx => ctx.Response.WriteAsync("pong"));
await server.RunAsync();Every registration this library owns hangs off one builder, in both hosting shapes:
var builder = HttpServer.CreateBuilder();
builder.Options.Port = 8080;
builder.AddAuthentication().AddJwtBearer(o => o.SigningKey = key);
builder.AddRateLimiter(o => o.GlobalPolicy = new FixedWindowRateLimitPolicy(100, TimeSpan.FromMinutes(1)));
builder.AddHealthChecks().AddServerCheck();
var app = builder.Build();
// …or, inside an app that already owns a container — the same calls:
services.AddShinyHttpServer(http =>
{
http.Options.Port = 8080;
http.AddHealthChecks().AddServerCheck();
http.Configure(server => server.MapMyAppEndpoints());
});Typed endpoints, generated at compile time:
[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 assemblyAn MCP server, on the same host, reachable from a MAUI app:
builder.Services
.AddMcpServer(o => o.ServerInfo = new() { Name = "thermostat", Version = "1.0.0" })
.WithTools<ThermostatTools>()
.WithHttpTransport();
var app = builder.Build();
app.MapMcp(); // POST/GET/DELETE/OPTIONS on /mcpThe MCP package is trim- and AOT-clean like the rest, with one thing the compiler cannot check for
you: a tool's parameter and return types are published as a JSON schema, and building that schema by
reflection does not survive trimming. Tools that trade only in primitives need nothing extra; give
the rest a source-generated context, and MapMcp() will tell you if you missed one.
[JsonSerializable(typeof(Query))]
[JsonSerializable(typeof(IReadOnlyList<Reading>))]
public partial class ToolJson : JsonSerializerContext;
.WithTools<ThermostatTools>(ToolJson.Default.Options)On a phone, where the server has to be found and has to survive the device moving:
builder.Services.AddShinyHttpServer(
http =>
{
http.Options.Address = IPAddress.Any;
// Keeps serving in the background on Android through a foreground service; on iOS, where
// nothing can, restores the server on resume if it was running. Both rebind on a network change.
http.AddHttpServerLifecycle(o => o.BackgroundMode = BackgroundServerMode.KeepAlive);
// Advertises on the local link, so the other device does not need an IP address.
http.AddHttpServerAdvertisement(o => o.ServiceType = "_myapp._tcp");
},
autoStart: false
);and on the other device:
var found = await locator.FindFirstAsync("_myapp._tcp");
using var client = new HttpClient { BaseAddress = found!.BaseAddress };The four tiers — one delegate, raw routes, middleware, and source-generated typed endpoints. Each is built on the one below and they compose in the same app.
| Core | Routing with constraints and runtime-mutable routes, ASP.NET-shaped middleware that can read and rewrite both bodies of an exchange, a real IServiceScope per request, results in both Results.* and IActionResult spellings, RFC 9457 problem details and an exception-handler chain, per-endpoint request timeouts, and a reverse proxy route |
| Caching | Conditional requests for handlers that are not serving a file — If-Match, If-None-Match, If-Modified-Since, 304 and 412 — plus output caching with a bounded in-memory store, where the saving is battery rather than bandwidth |
| Diagnostics | Health checks with liveness/readiness tags, telemetry on the in-box primitives — one Activity per request continuing the caller's traceparent, and the OpenTelemetry HTTP metrics an ASP.NET dashboard already reads — and W3C access logs, rolled and pruned, written off the request path |
| Formats | Content negotiation in both directions — responses chosen from Accept, request bodies from Content-Type. JSON out of the box; XML, MessagePack and protobuf are one line each, and a format of your own is an IOutputFormatter/IInputFormatter pair. XML and MessagePack need no dependency and no attributes on your DTOs: they read the same JsonTypeInfo the JSON path reads, which is what keeps them AOT-clean where XmlSerializer cannot be |
| Protocols | HTTP/1.1, HTTP/2 (own HPACK), HTTP/3 (own QPACK), WebSockets, Server-Sent Events, trailing headers on all three versions. Never guessed — ALPN over TLS, connection preface over cleartext. WebSockets carry permessage-deflate, keepalive pings, and a registry for broadcasting to a group |
| Content | Static files from disk or embedded resources, a published Blazor WebAssembly app, streaming multipart uploads, downloads with byte ranges and conditional GETs, a file browser over a directory, and brotli/gzip/deflate compression in both directions |
| Security | Authentication and authorization split ASP.NET-style, with Basic, API key, cookie and JWT schemes; policies, roles and claims; CORS, rate limiting and IP filtering, all with per-endpoint policies; signed double-submit antiforgery, the browser security headers, HSTS and an HTTPS redirect |
| TLS | Several endpoints with per-endpoint TLS, self-signed certificates generated in managed code (iOS and Android included), client certificates, and SPKI pinning for the app's own HttpClient |
| OpenAPI | An OpenAPI 3.0.3 document built entirely from compile-time metadata and your JsonSerializerContext — no reflection, no document object model |
| Tunnelling | A pluggable ITunnelProvider, the reference relay (both ends), SSH remote forwarding, zero-account quick tunnels, and Azure Relay |
| Mediator | Shiny.Mediator handlers published as endpoints — requests as JSON, commands as a status code, stream requests as Server-Sent Events, all bound at compile time |
| DocumentDb | A document type as a complete HTTP resource, with filtering, cursor paging, sparse fieldsets, ETag/If-Match, RFC 7396 merge-patch, a live SSE tail, and server-side scopes enforced on both sides of a write |
| gRPC | Unary, client-streaming, server-streaming and bidirectional methods, deadlines, per-message compression and status in trailers — plus gRPC-Web for browsers and anything on HTTP/1.1. Marshalling is yours, so nothing reflects over your messages |
| WebDAV | RFC 4918 classes 1 and 2 over a directory — PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK/UNLOCK, the If header and dead properties — so an app's storage mounts as a drive with no client to write |
| Lifecycle | Start, stop and restart at runtime, serialized and idempotent, with an observable state — an embedded server gets toggled, not just booted. It also follows the device: rebinding when the addresses change, and following the app between foreground and background |
| Discovery | The other half of hosting on a phone. mDNS advertises the server as the device moves and withdraws it when the server stops; the locator turns what is on the link into a base address a client can call |
| Testing | An in-memory transport, so an endpoint test costs no port and leaks no listener while still going through the real parser, router, middleware and framing |
Everything shipping targets net10.0 with the trim, AOT and single-file analyzers enabled, so
"AOT-clean" is enforced by the build rather than claimed in a readme.
Full docs are at shinylib.net/httpserver.
Shiny is free and will continue to be, but maintenance and support take a heavy toll on sustainability. If you or your company have the resources, please consider becoming a GitHub Sponsor.