Watson 7.0.0
dotnet add package Watson --version 7.0.0
NuGet\Install-Package Watson -Version 7.0.0
<PackageReference Include="Watson" Version="7.0.0" />
<PackageVersion Include="Watson" Version="7.0.0" />
<PackageReference Include="Watson" />
paket add Watson --version 7.0.0
#r "nuget: Watson, 7.0.0"
#:package Watson@7.0.0
#addin nuget:?package=Watson&version=7.0.0
#tool nuget:?package=Watson&version=7.0.0
Watson Webserver
Watson 7 is a simple, fast, async C# web server for building REST APIs and HTTP services with a unified programming model across HTTP/1.1, HTTP/2, and HTTP/3.
| Package | NuGet Version | Downloads |
|---|---|---|
| Watson | ||
| Watson.Core |
Special thanks to @DamienDennehy for allowing use of the Watson.Core package name in NuGet.
.NET Foundation
This project is part of the .NET Foundation.
What Is New In 7.0
Watson 7 is a major consumer-facing release:
- FastAPI-like REST experience -- SwiftStack functionality has been integrated directly into the server, giving Watson a native, first-class API route experience with automatic JSON serialization, typed parameter access, middleware, structured authentication, request timeouts, and health checks. No separate library required.
- Substantial performance improvements through hot-path optimization and removal of dependency on
http.sys - Native protocol selection through
WebserverSettings.Protocols; HTTP/1.1, HTTP/2, and HTTP/3 support - Runtime validation for unsupported protocol combinations
- HTTP/3 runtime normalization when QUIC is unavailable
- Alt-Svc support for advertising HTTP/3 endpoints
- Shared request and response semantics across protocols
- Built-in OpenAPI 3.0 document generation and Swagger UI
- Expanded automated coverage through
Test.AutomatedandTest.XUnit
Refer to CHANGELOG.md for the full release history.
Performance
Kestrel was treated as the gold standard throughout the 7.0 performance program, and that remains the benchmark Watson is chasing. The goal is not to pretend Watson has already reached Kestrel performance parity, because it hasn't. The goal is to keep closing the gap in throughput, response time, and requests per second while preserving Watson's programming model and correctness.
The important 7.0 story is that Watson has improved dramatically:
- Watson 6 is the real starting point for the 7.0 performance story. In a current short local validation run from this repository, Watson 6 delivered
437 req/son HTTP/1.1helloand330 req/son HTTP/1.1json, while Watson 7 delivered31,577 req/sand36,021 req/son the same scenarios. - After the architectural jump from Watson 6 to Watson 7, the optimization plan then started from a Watson 7 sustained HTTP/1.1 baseline of roughly
~25k req/sonhelloand~17k req/sonjson, against a Kestrel reference point of roughly~146k/~139k. - Across the 7.0 optimizations, Watson 7 repeatedly moved into the
~80k-95k req/srange on common HTTP/1.1 paths during longer benchmark runs, with corresponding latency improvements on the retained changes. - Those short-run numbers are environment-sensitive, but they still illustrate the magnitude of the architectural jump from the legacy Watson 6 host to Watson 7.
Just as important as the raw benchmark gains: Watson 7 no longer depends on http.sys. Watson 6 relied on the operating system HTTP stack. Watson 7 now runs on Watson-owned transport paths built around TcpListener for HTTP/1.1 and HTTP/2, plus QuicListener for HTTP/3 where available. That shift is foundational. It gives the library direct control over the hot path, which is why the retained 7.0 optimizations were possible at all and why there is still a realistic path to continue narrowing the gap to Kestrel over time.
In summary:
- Kestrel is still ahead overall and remains the standard against which we compare
- Watson 7 is dramatically ahead of Watson 6
- Watson 7 established the transport and protocol architecture needed to keep improving from here
Install
dotnet add package Watson
For normal server consumption, install Watson only. It depends on Watson.Core and NuGet will restore that dependency automatically.
Install Watson.Core directly only if you are building extensions, shared components, or tooling on top of the common abstractions without taking a direct dependency on the server package:
dotnet add package Watson.Core
Quick Start
using System;
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
public class Program
{
public static void Main(string[] args)
{
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
Console.WriteLine("Watson listening on http://127.0.0.1:9000");
Console.ReadLine();
server.Stop();
server.Dispose();
}
private static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Hello from Watson 7");
}
}
Then browse to http://127.0.0.1:9000/.
API Routes (FastAPI-Like Experience)
Watson 7 integrates SwiftStack functionality directly into the server, providing a FastAPI-like developer experience for building REST APIs. Route handlers receive an ApiRequest with typed parameter access and return objects that are automatically serialized to JSON.
Basic API Routes
using WatsonWebserver;
using WatsonWebserver.Core;
WebserverSettings settings = new WebserverSettings("127.0.0.1", 8080);
Webserver server = new Webserver(settings, DefaultRoute);
// GET route -- return value is auto-serialized to JSON
server.Get("/", async (req) => new { Message = "Hello, World!" });
// GET with typed URL parameters
server.Get("/users/{id}", async (req) =>
{
Guid id = req.Parameters.GetGuid("id");
int detail = req.Query.GetInt("detail", 0);
return new { Id = id, Detail = detail };
});
// POST with automatic JSON body deserialization
server.Post<CreateUserRequest>("/users", async (req) =>
{
CreateUserRequest body = req.GetData<CreateUserRequest>();
req.Http.Response.StatusCode = 201;
return new { Id = Guid.NewGuid(), body.Name, body.Email };
});
// POST without auto-deserialization (manual body access)
server.Post("/upload", async (req) =>
{
byte[] rawBytes = req.Http.Request.Data;
string rawText = req.Http.Request.DataAsString;
return new { Size = rawBytes?.Length ?? 0 };
});
// PUT and DELETE work the same way
server.Put<UpdateUserRequest>("/users/{id}", async (req) =>
{
UpdateUserRequest body = req.GetData<UpdateUserRequest>();
Guid id = req.Parameters.GetGuid("id");
return new { Updated = true, Id = id, body.Name };
});
server.Delete("/users/{id}", async (req) =>
{
Guid id = req.Parameters.GetGuid("id");
return new { Deleted = true, Id = id };
});
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
ctx.Response.StatusCode = 404;
await ctx.Response.Send("Not found");
}
Return Value Handling
API route handlers return Task<object>. Watson automatically processes the return value:
| Return Value | HTTP Behavior |
|---|---|
null |
Empty 200 response |
string |
text/plain response |
| Object or anonymous type | application/json serialized response |
(object, int) tuple |
JSON body with custom HTTP status code |
// Custom status code via tuple
server.Post<User>("/users", async (req) =>
{
User body = req.GetData<User>();
return (new { Id = Guid.NewGuid(), body.Name }, 201);
});
Error Handling
Throw WebserverException from any API route handler to return a structured JSON error response:
server.Get("/items/{id}", async (req) =>
{
Guid id = req.Parameters.GetGuid("id");
Item item = FindItem(id);
if (item == null)
throw new WebserverException(ApiResultEnum.NotFound, "Item not found");
return item;
});
Response:
{"Error":"NotFound","StatusCode":404,"Description":"The requested resource was not found.","Message":"Item not found"}
Typed Parameter Access
ApiRequest provides RequestParameters wrappers for URL parameters, query strings, and headers with type-safe accessors:
server.Get("/search", async (req) =>
{
string query = req.Query["q"];
int page = req.Query.GetInt("page", 1);
int size = req.Query.GetInt("size", 10);
bool active = req.Query.GetBool("active", true);
Guid tenantId = req.Headers.GetGuid("X-Tenant-Id");
return new { query, page, size, active, tenantId };
});
Available methods: GetInt, GetLong, GetDouble, GetDecimal, GetBool, GetGuid, GetDateTime, GetTimeSpan, GetEnum<T>, GetArray, TryGetValue<T>, Contains, GetKeys.
Middleware
Register middleware to run around every API route handler. Middleware executes in registration order. Call next() to continue the pipeline, or skip it to short-circuit.
// Logging middleware
server.Middleware.Add(async (ctx, next, token) =>
{
DateTime start = DateTime.UtcNow;
await next();
double ms = (DateTime.UtcNow - start).TotalMilliseconds;
Console.WriteLine($"{ctx.Request.Method} {ctx.Request.Url.RawWithoutQuery} -> {ctx.Response.StatusCode} ({ms:F1}ms)");
});
// Short-circuit middleware
server.Middleware.Add(async (ctx, next, token) =>
{
if (ctx.Request.RetrieveHeaderValue("X-Block") == "true")
{
ctx.Response.StatusCode = 403;
await ctx.Response.Send("Blocked");
return; // Don't call next()
}
await next();
});
Structured Authentication
Use AuthenticateApiRequest for structured authentication that returns an AuthResult. On failure, Watson automatically returns a 401 JSON response. On success, AuthResult.Metadata is propagated to req.Metadata in route handlers.
server.Routes.AuthenticateApiRequest = async (ctx) =>
{
string token = ctx.Request.RetrieveHeaderValue("Authorization");
if (token == "Bearer my-secret-token")
{
return new AuthResult
{
AuthenticationResult = AuthenticationResultEnum.Success,
AuthorizationResult = AuthorizationResultEnum.Permitted,
Metadata = new { UserId = 42, Role = "Admin" }
};
}
return new AuthResult
{
AuthenticationResult = AuthenticationResultEnum.NotFound,
AuthorizationResult = AuthorizationResultEnum.DeniedImplicit
};
};
// Public route (pre-authentication, the default)
server.Get("/public", async (req) => new { Public = true });
// Protected route (post-authentication)
server.Get("/admin", async (req) =>
{
return new { Secure = true, User = req.Metadata };
}, auth: true);
Request Timeouts
Enable request timeouts so that slow handlers receive a 408 response:
server.Settings.Timeout.DefaultTimeout = TimeSpan.FromSeconds(30);
server.Get("/slow", async (req) =>
{
// Pass req.CancellationToken to async operations for cooperative cancellation
await Task.Delay(60000, req.CancellationToken);
return new { Result = "Done" };
});
// Client receives: 408 {"Error":"RequestTimeout","Message":"The request timed out."}
Health Checks
Add a health check endpoint with an optional custom check delegate:
using WatsonWebserver.Core.Health;
server.UseHealthCheck(health =>
{
health.Path = "/health";
health.CustomCheck = async (token) =>
{
bool dbOk = await CheckDatabase(token);
return new HealthCheckResult
{
Status = dbOk ? HealthStatusEnum.Healthy : HealthStatusEnum.Unhealthy,
Description = dbOk ? "All systems operational" : "Database unavailable"
};
};
});
Returns HTTP 200 for Healthy/Degraded, HTTP 503 for Unhealthy.
Low-Level Routes
The traditional Func<HttpContextBase, Task> route signature is still fully supported and can be mixed freely with API routes:
// API route
server.Get("/api/users", async (req) => new { Users = new[] { "Alice", "Bob" } });
// Low-level route on the same server
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/legacy", async (ctx) =>
{
ctx.Response.StatusCode = 200;
await ctx.Response.Send("Low-level route");
});
Protocol Support
WebSockets
Watson 7 includes native server-side WebSocket support.
Current scope:
- HTTP/1.1 WebSockets are supported
- HTTP/2 and HTTP/3 WebSockets are not yet implemented
- Routing is Watson-native and works with the existing pre-auth and post-auth routing groups
- Whole-message receive semantics are exposed through
WebSocketSession - Same-path HTTP and WebSocket registration is supported
Example:
using System.Net.WebSockets;
using WatsonWebserver;
using WatsonWebserver.Core;
using WatsonWebserver.Core.WebSockets;
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
settings.WebSockets.Enable = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.Get("/chat", async req => new { Mode = "http" });
server.WebSocket("/chat", async (ctx, session) =>
{
await foreach (WebSocketMessage message in session.ReadMessagesAsync(ctx.Token))
{
if (message.MessageType == WebSocketMessageType.Text)
{
await session.SendTextAsync("echo:" + message.Text, ctx.Token);
}
}
});
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
ctx.Response.StatusCode = 404;
await ctx.Response.Send("Not found");
}
Session API
WebSocketSession exposes:
IdIsConnectedSubprotocolRemoteIpRemotePortRequestMetadataStatisticsReceiveAsync()ReadMessagesAsync()SendTextAsync()SendBinaryAsync()CloseAsync()
Statistics
Watson exposes counters at two levels:
WebSocketSession.Statisticsfor per-session message and byte countsserver.Statisticsfor aggregate HTTP and websocket payload counts plus active-connection counts
The aggregate server statistics include websocket application payload bytes. In the sample websocket server, the stats command reports those aggregate counters.
Settings
WebSocket settings live under WebserverSettings.WebSockets.
Important settings:
EnableMaxMessageSizeReceiveBufferSizeCloseHandshakeTimeoutMsAllowClientSuppliedGuidClientGuidHeaderNameSupportedVersionsEnableHttp1
Important defaults:
Enable = falseMaxMessageSize = 16777216ReceiveBufferSize = 65536CloseHandshakeTimeoutMs = 5000AllowClientSuppliedGuid = falseClientGuidHeaderName = "x-guid"SupportedVersions = ["13"]EnableHttp1 = true
TLS expectations
- Use
ws://when SSL is disabled - Use
wss://when SSL is enabled - The current implementation still uses the HTTP/1.1 upgrade path even when running over TLS
Current limitations
- WebSocket support is HTTP/1.1-only in the current implementation
- No raw underlying
System.Net.WebSockets.WebSocketis exposed publicly - Receive semantics are message-oriented and session-owned
- Optional subprotocol negotiation support is not yet exposed as a public configuration surface
See WEBSOCKETS_API.md for the focused WebSocket API guide and MIGRATING_FROM_WATSONWEBSOCKET.md for WatsonWebsocket migration guidance.
Default behavior
By default:
- HTTP/1.1 is enabled
- HTTP/2 is disabled
- HTTP/3 is disabled
Protocol matrix
| Protocol | Package | Notes |
|---|---|---|
| HTTP/1.1 | Watson |
Enabled by default |
| HTTP/2 over TLS | Watson |
Supported |
HTTP/2 cleartext prior knowledge (h2c) |
Watson |
Supported only when explicitly enabled |
| HTTP/3 over TLS/QUIC | Watson |
Supported when QUIC is available |
Important rules
Watson validates protocol settings before startup:
- At least one protocol must be enabled
- HTTP/2 without TLS requires
EnableHttp2Cleartext = true - HTTP/2 cleartext support is prior-knowledge mode, not opportunistic upgrade mode
- HTTP/3 requires TLS
AltSvc.Enabledrequires HTTP/3 to be enabled- If HTTP/3 is enabled but QUIC is unavailable at runtime, Watson disables HTTP/3 and Alt-Svc for that server start
Enable HTTP/2 over TLS
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
WebserverSettings settings = new WebserverSettings("localhost", 8443, true);
settings.Ssl.PfxCertificateFile = "server.pfx";
settings.Ssl.PfxCertificatePassword = "password";
settings.Protocols.EnableHttp1 = true;
settings.Protocols.EnableHttp2 = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("HTTP/1.1 and HTTP/2 enabled");
}
Enable HTTP/2 cleartext prior knowledge
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
WebserverSettings settings = new WebserverSettings("127.0.0.1", 8080);
settings.Protocols.EnableHttp1 = true;
settings.Protocols.EnableHttp2 = true;
settings.Protocols.EnableHttp2Cleartext = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("HTTP/1.1 and h2c prior knowledge enabled");
}
Enable HTTP/3 and Alt-Svc
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
WebserverSettings settings = new WebserverSettings("localhost", 8443, true);
settings.Ssl.PfxCertificateFile = "server.pfx";
settings.Ssl.PfxCertificatePassword = "password";
settings.Protocols.EnableHttp1 = true;
settings.Protocols.EnableHttp2 = true;
settings.Protocols.EnableHttp3 = true;
settings.AltSvc.Enabled = true;
settings.AltSvc.Http3Alpn = "h3";
settings.AltSvc.MaxAgeSeconds = 86400;
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("HTTP/1.1, HTTP/2, and HTTP/3 enabled");
}
If QUIC is unavailable on the current machine, Watson will disable HTTP/3 and Alt-Svc for that startup rather than advertise a protocol it cannot serve.
Core Configuration Surface
The primary configuration object is WebserverSettings.
Important areas for consumers:
ProtocolsEnableHttp1EnableHttp2EnableHttp3EnableHttp2CleartextIdleTimeoutMsMaxConcurrentStreamsHttp2Http3
AltSvcEnabledAuthorityPortHttp3AlpnMaxAgeSeconds
IOStreamBufferSizeMaxRequestsReadTimeoutMsMaxIncomingHeadersSizeEnableKeepAliveMaxRequestBodySizeMaxHeaderCount
SslEnableSslCertificatePfxCertificateFilePfxCertificatePasswordMutuallyAuthenticateAcceptInvalidAcertificates
HeadersDefaultHeadersIncludeContentLength
AccessControlDebugUseMachineHostname
HTTP/3 tuning example
WebserverSettings settings = new WebserverSettings("localhost", 8443, true);
settings.Ssl.PfxCertificateFile = "server.pfx";
settings.Ssl.PfxCertificatePassword = "password";
settings.Protocols.EnableHttp3 = true;
settings.Protocols.Http3.MaxFieldSectionSize = 32768;
settings.Protocols.Http3.QpackMaxTableCapacity = 4096;
settings.Protocols.Http3.QpackBlockedStreams = 16;
settings.Protocols.Http3.EnableDatagram = false;
Request Consumption
The most important 7.0 consumption rule is this:
- For protocol-agnostic body handling, use
DataAsBytes,DataAsString, orReadBodyAsync() - Only use
ReadChunk()when you are explicitly handling HTTP/1.1 chunked transfer-encoding
Recommended protocol-agnostic body read
Use DataAsBytes when synchronous first-read semantics are acceptable:
private static async Task EchoBody(HttpContextBase ctx)
{
byte[] body = ctx.Request.DataAsBytes;
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "application/octet-stream";
await ctx.Response.Send(body, ctx.Token);
}
Use ReadBodyAsync() when you want an explicit async body read with cancellation support.
Read as string
private static async Task EchoText(HttpContextBase ctx)
{
string text = ctx.Request.DataAsString;
ctx.Response.ContentType = "text/plain";
await ctx.Response.Send("You sent: " + text, ctx.Token);
}
DataAsBytes and DataAsString fully read the body on first access. ReadBodyAsync() does the same thing explicitly and asynchronously. After any of those first reads, the body is cached.
HTTP/1.1 chunked request bodies
private static async Task UploadData(HttpContextBase ctx)
{
if (ctx.Request.Protocol != HttpProtocol.Http1 || !ctx.Request.ChunkedTransfer)
{
ctx.Response.StatusCode = 400;
await ctx.Response.Send("Expected HTTP/1.1 chunked request body", ctx.Token);
return;
}
bool finalChunk = false;
while (!finalChunk)
{
Chunk chunk = await ctx.Request.ReadChunk(ctx.Token);
finalChunk = chunk.IsFinalChunk;
if (chunk.Data != null && chunk.Data.Length > 0)
{
// Process the chunk payload here
}
}
await ctx.Response.Send("Chunked upload complete", ctx.Token);
}
ReadChunk() is not available for HTTP/2 or HTTP/3 requests and throws if used there.
Response Patterns
Simple text or bytes
private static async Task GetHello(HttpContextBase ctx)
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "text/plain";
await ctx.Response.Send("Hello", ctx.Token);
}
Stream a known-length payload
using System.IO;
private static async Task DownloadFile(HttpContextBase ctx)
{
using (FileStream fileStream = new FileStream("large.bin", FileMode.Open, FileAccess.Read))
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "application/octet-stream";
await ctx.Response.Send(fileStream.Length, fileStream, ctx.Token);
}
}
Chunked or streaming response semantics
SendChunk() works across supported protocols, but the wire behavior depends on the protocol:
- HTTP/1.1: literal
Transfer-Encoding: chunked - HTTP/2 and HTTP/3: streamed response semantics on framed transports
private static async Task StreamData(HttpContextBase ctx)
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "text/plain";
ctx.Response.ChunkedTransfer = true;
await ctx.Response.SendChunk(System.Text.Encoding.UTF8.GetBytes("part 1\n"), false, ctx.Token);
await ctx.Response.SendChunk(System.Text.Encoding.UTF8.GetBytes("part 2\n"), false, ctx.Token);
await ctx.Response.SendChunk(Array.Empty<byte>(), true, ctx.Token);
}
Server-sent events
private static async Task SendEvents(HttpContextBase ctx)
{
ctx.Response.StatusCode = 200;
ctx.Response.ServerSentEvents = true;
for (Int32 i = 1; i <= 5; i++)
{
ServerSentEvent serverEvent = new ServerSentEvent
{
Id = i.ToString(),
Event = "counter",
Data = "Event " + i.ToString()
};
bool isFinal = i == 5;
await ctx.Response.SendEvent(serverEvent, isFinal, ctx.Token);
}
}
Routing
Watson routes requests in this order:
PreflightPreRoutingPreAuthenticationStaticContentParameterDynamic
AuthenticateRequestPostAuthenticationStaticContentParameterDynamic
DefaultPostRouting
Route example
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
Webserver server = new Webserver(settings, DefaultRoute);
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/hello", GetHelloRoute);
server.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/users/{id}", GetUserRoute);
server.Routes.PreAuthentication.Dynamic.Add(HttpMethod.GET, new Regex("^/items/\\d+$"), GetItemRoute);
server.Routes.PreAuthentication.Content.Add("./wwwroot", true);
server.Start();
static async Task GetHelloRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Hello from a static route");
}
static async Task GetUserRoute(HttpContextBase ctx)
{
String id = ctx.Request.Url.Parameters["id"];
await ctx.Response.Send("User " + id);
}
static async Task GetItemRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Dynamic route match");
}
static async Task DefaultRoute(HttpContextBase ctx)
{
ctx.Response.StatusCode = 404;
await ctx.Response.Send("Not found");
}
Exception handler per route
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/boom", BoomRoute, BoomExceptionRoute);
static async Task BoomRoute(HttpContextBase ctx)
{
throw new Exception("Whoops");
}
static async Task BoomExceptionRoute(HttpContextBase ctx, Exception e)
{
ctx.Response.StatusCode = 500;
await ctx.Response.Send(e.Message);
}
Authentication And Metadata
Authentication is typically implemented in Routes.AuthenticateRequest. If authentication succeeds, place user or session data in ctx.Metadata and continue. If it fails, send a response there and return.
server.Routes.AuthenticateRequest = AuthenticateRequest;
static async Task AuthenticateRequest(HttpContextBase ctx)
{
if (ctx.Request.RetrieveHeaderValue("X-Api-Key") != "secret")
{
ctx.Response.StatusCode = 401;
await ctx.Response.Send("Unauthorized");
return;
}
ctx.Metadata = "authenticated-user";
}
Avoid sending a second response from PostRouting if a response has already been completed.
Access Control
By default, Watson permits all inbound connections.
server.Settings.AccessControl.Mode = AccessControlMode.DefaultPermit;
server.Settings.AccessControl.DenyList.Add("127.0.0.1", "255.255.255.255");
To default-deny and only allow certain addresses:
server.Settings.AccessControl.Mode = AccessControlMode.DefaultDeny;
server.Settings.AccessControl.PermitList.Add("192.168.1.0", "255.255.255.0");
HostBuilder
HostBuilder offers a fluent setup API over Webserver.
using System.Threading.Tasks;
using WatsonWebserver;
using WatsonWebserver.Core;
using WatsonWebserver.Extensions.HostBuilderExtension;
Webserver server = new HostBuilder("127.0.0.1", 8000, false, DefaultRoute)
.MapStaticRoute(HttpMethod.GET, "/links", GetLinksRoute)
.MapStaticRoute(HttpMethod.POST, "/login", LoginRoute)
.MapDefaultRoute(DefaultRoute)
.Build();
server.Start();
static async Task GetLinksRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Here are your links");
}
static async Task LoginRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Checking credentials");
}
static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Hello from the default route");
}
OpenAPI / Swagger
OpenAPI support is built in. No extra package is required beyond Watson or Watson.Core.
Enable OpenAPI
using WatsonWebserver;
using WatsonWebserver.Core;
using WatsonWebserver.Core.OpenApi;
WebserverSettings settings = new WebserverSettings("localhost", 8080);
Webserver server = new Webserver(settings, DefaultRoute);
server.UseOpenApi(openApi =>
{
openApi.Info.Title = "My API";
openApi.Info.Version = "1.0.0";
openApi.Info.Description = "Example API";
});
server.Start();
static async Task DefaultRoute(HttpContextBase ctx)
{
await ctx.Response.Send("Hello");
}
Endpoints:
- OpenAPI JSON:
/openapi.json - Swagger UI:
/swagger
Document routes
server.Routes.PreAuthentication.Static.Add(
HttpMethod.GET,
"/api/users",
GetUsersHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Get users", "Users")
.WithDescription("Returns all users")
.WithParameter(OpenApiParameterMetadata.Query("active", "Active-only filter", false, OpenApiSchemaMetadata.Boolean()))
.WithResponse(200, OpenApiResponseMetadata.Json(
"Users",
OpenApiSchemaMetadata.CreateArray(OpenApiSchemaMetadata.CreateRef("User")))));
Hostname Handling
WebserverSettings.UseMachineHostname controls the host value Watson uses when composing response host metadata.
- When
Hostnameis*or+, Watson forcesUseMachineHostname = true - For named hosts and concrete addresses, it is disabled by default
- You can opt in manually by setting
UseMachineHostname = true
Example:
WebserverSettings settings = new WebserverSettings("localhost", 9000);
settings.UseMachineHostname = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
Accessing From Outside Localhost
Watson
When you bind Watson to 127.0.0.1 or localhost, only the local machine can reach it.
To expose Watson externally:
- Bind to the exact DNS hostname,
0.0.0.0,*, or+as appropriate - Ensure the HTTP
Hostheader matches the binding constraints that apply to your environment - Open the firewall port
- Run elevated when the operating system requires it
- Configure URL ACLs or certificate bindings when your OS requires them
On Windows, netsh http show urlacl and netsh http add urlacl ... are commonly needed when binding outside localhost.
Operational Notes
- If
Settings.Ssl.Enableistrue, configureSslCertificateor a PFX file before callingStart() - If you enable HTTP/3, also plan for QUIC runtime availability on the target machine
- If you enable
IO.MaxRequestBodySize, Watson enforces it against declared request body sizes - If you enable
IO.MaxHeaderCount, Watson limits inbound header cardinality - For long-lived HTTP/1.1 workloads,
IO.EnableKeepAlivecan reduce connection churn - Debug logging options live under
Settings.Debug; wire a logger intoserver.Events.Loggerif you want to receive those messages
Running In Docker
Refer to src/Test.Docker and its companion documentation.
Testing
Automated validation is covered by:
src/Test.Automated: integration tests with real HTTP requests against a running serversrc/Test.XUnit: CI-oriented test runner that executes shared test logic fromTest.Sharedwithout invokingTest.Automatedsrc/Test.RestApi: interactive server demonstrating all API route features (run and test manually with curl/Postman)src/Test.Benchmark: benchmark harness for cross-target and cross-protocol performance comparisons
Recommended commands:
dotnet run --project src\Test.Automated\Test.Automated.csproj
powershell -ExecutionPolicy Bypass -File src\Test.XUnit\Run-Test.XUnit.ps1
To see each xUnit test with pass/fail status and runtime in the console:
dotnet test src\Test.XUnit\Test.XUnit.csproj --no-build -c Debug -f net10.0 --logger "console;verbosity=detailed"
For more detail, refer to TESTING.md.
Special Thanks
Thanks to the contributors who have helped improve Watson Webserver:
- @notesjor @shdwp @Tutch @GeoffMcGrath @jurkovic-nikola @joreg @Job79 @at1993 @MartyIX
- @pocsuka @orinem @deathbull @binozo @panboy75 @iain-cyborn @gamerhost31 @lucafabbri
- @nhaberl @grgouala @sapurtcomputer30 @winkmichael @sqlnew @SaintedPsycho @Return25
- @marcussacana @samisil @Jump-Suit @ChZhongPengCheng33 @bobaoapae @rodgers-r
- @john144 @zedle @GitHubProUser67 @bemoty @bemon @nomadeon @Infiziert90 @kyoybs
Version History
Refer to CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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. |
-
net10.0
- Watson.Core (>= 7.0.0)
-
net8.0
- Watson.Core (>= 7.0.0)
NuGet packages (19)
Showing the top 5 NuGet packages that depend on Watson:
| Package | Downloads |
|---|---|
|
S3Server
Emulated Amazon Web Services (AWS) Simple Storage Service (S3) server-side interface. |
|
|
S3ServerInterface
Emulated Amazon Web Services (AWS) Simple Storage Service (S3) server-side interface. |
|
|
Komodo.Server
Komodo is an information search, metadata, storage, and retrieval platform. Komodo.Server is a standalone RESTful server. Use Komodo.Daemon if you wish to integrate search directly within your application. |
|
|
RestDb
RestDb is a platform that enables a RESTful API interface in front of Microsoft SQL Server, MySQL, PostgreSQL, and Sqlite databases. |
|
|
Kvpbase.StorageServer
Scalable, simple RESTful object storage platform. |
GitHub repositories (2)
Showing the top 2 popular GitHub repositories that depend on Watson:
| Repository | Stars |
|---|---|
|
rocksdanister/lively
Free and open-source software that allows users to set animated desktop wallpapers and screensavers powered by WinUI 3.
|
|
|
litegraphdb/litegraph
Lightweight graph database with relational, vector, and MCP support, designed to power knowledge and artificial intelligence persistence and retrieval.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 7.0.0 | 39 | 3/30/2026 |
| 6.6.0 | 524 | 3/22/2026 |
| 6.5.5 | 3,088 | 2/5/2026 |
| 6.5.3 | 149 | 2/5/2026 |
| 6.5.1 | 2,019 | 12/24/2025 |
| 6.5.0 | 418 | 12/21/2025 |
| 6.4.0 | 5,687 | 10/17/2025 |
| 6.3.17 | 4,410 | 10/13/2025 |
| 6.3.15 | 3,305 | 9/19/2025 |
| 6.3.14 | 297 | 9/19/2025 |
| 6.3.13 | 2,146 | 9/4/2025 |
| 6.3.12 | 2,763 | 8/6/2025 |
| 6.3.10 | 2,302 | 6/9/2025 |
| 6.3.9 | 3,194 | 4/1/2025 |
| 6.3.8 | 334 | 4/1/2025 |
| 6.3.7 | 478 | 3/29/2025 |
| 6.3.5 | 10,923 | 1/20/2025 |
| 6.3.4 | 6,429 | 1/7/2025 |
| 6.3.3 | 334 | 1/2/2025 |
| 6.3.2 | 653 | 12/13/2024 |
Watson 7.0 foundation: shared semantic pipeline extraction, protocol configuration surface, lifecycle metadata, and fail-fast protocol validation