A layered, AOT-native networking stack for .NET — from raw TCP/UDP sockets to a fully featured HTTP web framework.
English | 简体中文 | 繁體中文 | Deutsch | Español | Français | 日本語 | Português (Brasil) | Русский
┌─────────────────────────────────────────────────────────────┐
│ PicoNode: layered networking for .NET │
│ ✓ Raw TCP/UDP socket transports with async I/O │
│ ✓ HTTP/1.1 + HTTP/2 + WebSocket protocols │
│ ✓ Web framework with middleware, routing, static files │
│ ✓ Integrated with PicoHex ecosystem (PicoDI/PicoLog/PicoCfg)│
│ ✓ Native AOT compatible across all net10.0 layers │
│ ✓ Minimal runtime dependencies │
└─────────────────────────────────────────────────────────────┘
| Feature | PicoNode | ASP.NET Core |
|---|---|---|
| Dependency model | Zero required runtime deps; layer pick-and-choose | Microsoft.AspNetCore.App framework reference |
| Request parsing | Span-based streaming, zero-copy System.IO.Pipelines |
String-based with IO.Pipelines adapter |
| HTTP/2 | Inline HPACK decoder, frame-level control | Transparent via Kestrel; limited low-level access |
| AOT Support | ✅ Native — all net10.0 libraries | |
| DI / Logging / Config | PicoDI + PicoLog + PicoCfg (PicoHex native) | Microsoft.Extensions.* |
| WebSocket | RFC 6455 frame codec with message handler abstraction | Transparent via middleware |
| Line count | ~15K for the full stack | ~1M+ for ASP.NET Core |
Design priority: PicoNode prioritizes allocation efficiency and AOT compatibility.
ValueTaskon hot-path delegates, ArrayPool-based buffer management, and optional delegates (no forced allocations) are deliberate trade-offs — they keep the transport layer compact and predictable.
PicoNode is part of the PicoHex family and integrates natively with:
| Library | Purpose | NuGet |
|---|---|---|
| PicoDI | Zero-reflection compile-time DI | PicoDI.Abs |
| PicoLog | Structured logging with AOT safety | PicoLog.Abs |
| PicoCfg | Source-generated configuration binding | PicoCfg.Abs |
PicoNode.Abs Core interfaces (netstandard2.0, zero deps)
↓
PicoNode TCP & UDP transports + ILogger (net10.0)
↓
PicoNode.Http HTTP/1.1 + HTTP/2 + WebSocket (net10.0)
↓
PicoNode.Web Web framework + PicoDI ISvcContainer (net10.0)
↓
PicoWeb Ready-to-run web server + PicoCfg (net10.0)
dotnet add package PicoNodeInstalling
PicoNodebrings in the TCP/UDP transport. ReferencePicoNode.HttporPicoNode.Webfor higher-level layers.
PicoNode ships as layered NuGet packages. Pick exactly the abstraction level you need:
| Package | Install when… | What you get |
|---|---|---|
| PicoWeb | You want a ready-to-run web server | WebServer + WebApp + HTTP + TCP (all transitive) |
| PicoNode.Web | You want the web framework without hosting | WebApp, routing, middleware, static files, DI |
| PicoNode.Http | You want raw HTTP protocol handling | HTTP/1.1 + HTTP/2 + WebSocket, HttpRouter |
| PicoNode | You want raw TCP/UDP transports | TcpNode, UdpNode, socket lifecycle, metrics |
| PicoNode.Abs | You're writing a handler or extension | INode, ITcpConnectionHandler, core contracts |
PicoWeb → PicoNode.Web → PicoNode.Http → PicoNode → PicoNode.Abs
(host) (web/DI) (HTTP) (transport) (interfaces)
using System.Net;
using PicoNode;
using PicoNode.Abs;
var node = new TcpNode(new TcpNodeOptions
{
Endpoint = new IPEndPoint(IPAddress.Loopback, 7001),
ConnectionHandler = new EchoHandler(),
});
await node.StartAsync();
Console.ReadLine();
await node.DisposeAsync();
sealed class EchoHandler : ITcpConnectionHandler
{
public Task OnConnectedAsync(ITcpConnectionContext c, CancellationToken ct)
=> Task.CompletedTask;
public Task OnClosedAsync(ITcpConnectionContext c, TcpCloseReason r,
Exception? e, CancellationToken ct) => Task.CompletedTask;
public ValueTask<SequencePosition> OnReceivedAsync(
ITcpConnectionContext connection,
ReadOnlySequence<byte> buffer,
CancellationToken ct)
{
_ = connection.SendAsync(buffer, ct);
return ValueTask.FromResult(buffer.End);
}
}using System.Net;
using PicoNode;
using PicoNode.Http;
var node = new TcpNode(new TcpNodeOptions
{
Endpoint = new IPEndPoint(IPAddress.Loopback, 7002),
ConnectionHandler = new HttpConnectionHandler(new HttpConnectionHandlerOptions
{
RequestHandler = new HttpRouter(new HttpRouterOptions
{
Routes =
[
HttpRoute.MapGet("/", static (_, _) =>
ValueTask.FromResult(new HttpResponse
{
StatusCode = 200, ReasonPhrase = "OK",
Headers = [new("Content-Type", "text/plain")],
Body = "Hello from PicoNode.Http"u8.ToArray(),
})),
],
}).HandleAsync,
ServerHeader = "PicoNode",
}),
});
await node.StartAsync();
Console.ReadLine();
await node.DisposeAsync();using PicoNode.Web;
using PicoWeb;
var api = new WebApiBuilder()
.ConfigureApp(o => o.ServerHeader = "MyApp")
.RegisterScoped<IUserService, UserService>()
.Build();
api.MapGet("/", (WebContext ctx) =>
Results.Text(200, "Hello, World!"));
api.MapGet("/users/{id}", async (WebContext ctx, IUserService svc) =>
{
var user = await svc.GetByIdAsync(ctx.RouteValues["id"]);
var bytes = PicoJetson.JsonSerializer.SerializeToUtf8Bytes(user);
return Results.Json(200, bytes);
});
api.MapPost("/echo", async (WebContext ctx) =>
{
using var reader = new StreamReader(ctx.Request.BodyStream);
var body = await reader.ReadToEndAsync();
return Results.Text(200, body);
});
await api.RunAsync("http://+:8080");// Controllers/UsersController.cs
using PicoJetson;
public class UsersController
{
public UserDto GetUser(int id) { return new UserDto { Id = id }; }
}
// Program.cs
var api = new WebApiBuilder()
.RegisterScoped<UsersController>()
.Build();
// Controllers.Gen auto-generates endpoint stubs + [PicoJsonSerializable]
await api.RunAsync("http://+:8080");PicoNode supports two configuration modes:
var options = new TcpNodeOptions
{
Endpoint = new IPEndPoint(IPAddress.Any, 8080),
MaxConnections = 500,
IdleTimeout = TimeSpan.FromMinutes(5),
};
var node = new TcpNode(options);var config = await Cfg.CreateBuilder()
.Add(new Dictionary<string, string>
{
["App:Name"] = "PicoCfg",
["App:Enabled"] = "true",
})
.BuildAsync();
var settings = CfgBind.Bind<AppSettings>(config, "App");
public sealed class AppSettings
{
public string? Name { get; set; }
public bool Enabled { get; set; }
}// TcpNode supports runtime config reload (except Endpoint)
var options = new TcpNodeOptions
{
Endpoint = new IPEndPoint(IPAddress.Loopback, 8080),
Config = config, // ICfgRoot for live reload
};
// Node starts a reload loop watching for config changes| Option | Default | Description |
|---|---|---|
Endpoint |
(required) | Local endpoint to bind |
ConnectionHandler |
(required) | ITcpConnectionHandler |
MaxConnections |
1000 | Maximum concurrent connections |
IdleTimeout |
2 min | Time before idle connections are closed |
DrainTimeout |
5 sec | Grace period on shutdown |
SslOptions |
null |
TLS/SSL configuration |
NoDelay |
true |
TCP_NODELAY (Nagle disabled) |
Logger |
null |
PicoLog ILogger for structured diagnostics |
| Option | Default | Description |
|---|---|---|
Endpoint |
(required) | Local endpoint to bind |
DatagramHandler |
(required) | IUdpDatagramHandler |
DispatchWorkerCount |
1 | Concurrent datagram workers |
DatagramQueueCapacity |
1024 | Per-worker queue depth |
QueueOverflowMode |
DropNewest |
Behavior when queues are full |
Logger |
null |
PicoLog ILogger |
| Option | Default | Description |
|---|---|---|
RequestHandler |
(required) | HttpRequestHandler delegate |
ServerHeader |
null |
Value for the Server header |
MaxRequestBytes |
8192 | Maximum request size in bytes |
Logger |
null |
PicoLog ILogger |
PicoNode uses PicoLog for structured diagnostics. All non-fatal errors are logged with operation context:
var logger = new LoggerFactory([new ConsoleSink()])
.CreateLogger("PicoNode.Tcp");
var node = new TcpNode(new TcpNodeOptions
{
Endpoint = new IPEndPoint(IPAddress.Loopback, 7001),
ConnectionHandler = handler,
Logger = logger, // All transport faults logged here
});
// Log output:
// [Error] Operation tcp.accept failed: AcceptFailed - System.Net.Sockets.SocketException
// [Warning] Operation tcp.reject.limit failed: SessionRejected
// [Debug] Socket shutdown during TLS teardown failedLog levels by fault code:
Error: StartFailed, StopFailed, AcceptFailed, ReceiveFailed, SendFailed, HandlerFailed, TlsFailed, DatagramReceiveFailed, DatagramHandlerFailedWarning: SessionRejected, DatagramDroppedDebug: Socket shutdown during cleanup (best-effort operations)
PicoNode.Web requires ISvcContainer at construction time (DI First). Scopes are created per-request automatically.
using PicoNode.Web;
using PicoWeb;
using PicoJetson;
var container = new SvcContainer();
container.RegisterScoped<IDatabase, SqlDatabase>();
var app = new WebApp(container);
app.MapGet("/db", async (WebContext ctx) =>
{
var db = ctx.Services.GetService<IDatabase>() as IDatabase;
var data = await db!.QueryAsync("...");
var bytes = PicoJetson.JsonSerializer.SerializeToUtf8Bytes(data);
return Results.Json(200, bytes);
});
app.Build();Handler parameters are automatically resolved (requires using PicoNode.Web;):
WebContext→ current contextCancellationToken→ request cancellation token- Any registered service → resolved from DI scope
app.MapGet("/users/{id}", async (WebContext ctx, IUserService svc) =>
{
var user = await svc.GetByIdAsync(ctx.RouteValues["id"]);
var bytes = PicoJetson.JsonSerializer.SerializeToUtf8Bytes(user);
return Results.Json(200, bytes);
});PicoJetson source generators run at compile time. Handlers must call SerializeToUtf8Bytes<T>() directly in user code to trigger generator:
// ✅ Triggers PicoJetson.Gen — UserDto serializer generated
var bytes = PicoJetson.JsonSerializer.SerializeToUtf8Bytes(user);
// ❌ Does NOT trigger generator (cross-assembly generic)
Results.Json<UserDto>(200, user);using PicoNode.Web;
using PicoWeb;
var api = new WebApiBuilder()
.RegisterScoped<IUserService, UserService>()
.ConfigureJson(o => o.PropertyNamingPolicy = JsonNamingPolicy.CamelCase)
.Build();
api.MapGet("/api/users/{id}", async (WebContext ctx, IUserService svc) =>
{
var user = await svc.GetByIdAsync(ctx.RouteValues["id"]);
var bytes = PicoJetson.JsonSerializer.SerializeToUtf8Bytes(user);
return Results.Json(200, bytes);
});
await api.RunAsync("http://+:5000");// 1. Controller in Controllers/ folder (convention)
// Controllers/UsersController.cs
public class UsersController
{
public UserDto GetUser(int id) { return new UserDto { ... }; }
public List<UserDto> GetAllUsers() { return ...; }
}
// 2. Register controller in DI
builder.RegisterScoped<UsersController>();
// 3. Call EndpointRegistrar (auto-generated by Controllers.Gen)
EndpointRegistrar.RegisterAll(app);
// 4. Or use WebApiBuilder (calls it automatically)
new WebApiBuilder()
.RegisterScoped<UsersController>()
.Build()
.RunAsync("http://+:5000");Controllers.Gen and PicoWeb.Gen source generators:
- Scan
Controllers/folder andapp.MapGet/MapPostcalls - Generate
[PicoJsonSerializable]for discovered DTOs - Generate endpoint stubs that resolve controllers from DI
Note: The controller-based pattern requires PicoJetson.Gen for automatic DTO serialization registration. For the MapXX pattern, call
PicoJetson.JsonSerializer.SerializeToUtf8Bytes<T>()explicitly in your handler.
## Built-in Middleware
### Compression
```csharp
var compression = new CompressionMiddleware(
CompressionLevel.Fastest, minimumBodySize: 860);
app.Use(compression.InvokeAsync);
Supports Brotli, Gzip, and Deflate. Auto-selects the best encoding from the client's Accept-Encoding header.
var staticFiles = new StaticFileMiddleware(
"/path/to/wwwroot", requestPathPrefix: "/static");
app.Use(staticFiles.InvokeAsync);Serves files from a root directory. Prevents directory traversal. Maps 30+ file extensions to MIME types.
app.Use(async (ctx, next, ct) =>
{
var corsOptions = new CorsOptions
{
AllowedOrigins = ["https://example.com"],
AllowedMethods = ["GET", "POST"],
AllowCredentials = true,
};
var preflight = CorsHandler.HandlePreflight(ctx.Request, corsOptions);
if (preflight is not null)
return preflight;
var response = await next(ctx, ct);
// Add CORS response headers
foreach (var header in CorsHandler.GetResponseHeaders(ctx.Request, corsOptions))
{
response.Headers.Add(header.Key, header.Value);
}
return response;
});// Cookie parsing
var cookies = CookieParser.Parse(context.Request.HeaderFields);
// Set-Cookie
var setCookie = new SetCookieBuilder("session", "abc123")
.Path("/").HttpOnly().Secure().SameSite("Strict").MaxAge(3600)
.Build();
// Multipart form data
var form = MultipartFormDataParser.Parse(context.Request);
foreach (var field in form?.Fields ?? [])
Console.WriteLine($"{field.Name} = {field.Value}");
foreach (var file in form?.Files ?? [])
Console.WriteLine($"{file.FileName}: {file.ContentType} ({file.Content.Length} bytes)");Both TcpNode and UdpNode expose real-time counters:
// TCP
var tcpMetrics = node.GetMetrics(); // only TcpNode
Console.WriteLine($"Accepted: {tcpMetrics.TotalAccepted}");
Console.WriteLine($"Active: {tcpMetrics.ActiveConnections}");
Console.WriteLine($"Sent: {tcpMetrics.TotalBytesSent}");
Console.WriteLine($"Received: {tcpMetrics.TotalBytesReceived}");
// UDP counters available via internal state
// (UdpNode tracks datagrams, bytes, and drops internally)
## Projects
| Project | Target | Description |
|---------|--------|-------------|
| **PicoNode.Abs** | netstandard2.0 | Core interfaces: `INode`, `ITcpConnectionHandler`, `IUdpDatagramHandler`, fault codes, enums |
| **PicoNode** | net10.0 | `TcpNode` and `UdpNode` — production-grade async socket transports |
| **PicoNode.Http** | net10.0 | `HttpConnectionHandler`, `HttpRouter` — HTTP/1.1, HTTP/2, WebSocket |
| **PicoNode.Web** | net10.0 | `WebApp`, `WebRouter`, middleware, static files, compression, CORS, DI |
| **PicoWeb** | net10.0 | `WebServer` — thin host wiring `WebApp` to `TcpNode` |
## Samples
| Sample | Port | Description |
|--------|------|-------------|
| `PicoNode.Samples.Echo` | 7001 (TCP), 7002 (UDP) | Raw TCP/UDP echo server |
| `PicoNode.Samples.Http` | 7003 | HTTP routing with `HttpRouter` |
| `PicoWeb.Samples` | 7004 | Full web app with middleware and DI |
```bash
dotnet run --project samples/PicoWeb.Samples/PicoWeb.Samples.csproj
# Build the entire solution
dotnet build PicoNode.slnx -c Release
# Run all tests
dotnet test --solution PicoNode.slnx -c Release
# Run a specific test project
dotnet test --project tests/PicoNode.Http.Tests/PicoNode.Http.Tests.csproj -c Release
# AOT publish check
dotnet publish src/PicoWeb/PicoWeb.csproj -c Release -r win-x64 -p:PublishAot=trueMicrobenchmarks are provided via PicoBench:
dotnet run --project benchmarks/PicoNode.Http.Benchmarks/PicoNode.Http.Benchmarks.csproj -c Release -- quickBenchmarks cover HTTP parsing, router dispatch (hit/miss/405), full pipeline, and localhost round-trips.
- .NET 10.0+ (PicoNode, PicoNode.Http, PicoNode.Web, PicoWeb)
- .NET Standard 2.0 (PicoNode.Abs — maximum compatibility)
- PicoHex ecosystem (optional): PicoDI, PicoLog, PicoCfg
MIT © 2025 XiaoFei Du
PicoNode — layered networking for .NET