![]() |
VOOZH | about |
dotnet add package Watson --version 7.0.14
NuGet\Install-Package Watson -Version 7.0.14
<PackageReference Include="Watson" Version="7.0.14" />
<PackageVersion Include="Watson" Version="7.0.14" />Directory.Packages.props
<PackageReference Include="Watson" />Project file
paket add Watson --version 7.0.14
#r "nuget: Watson, 7.0.14"
#:package Watson@7.0.14
#addin nuget:?package=Watson&version=7.0.14Install as a Cake Addin
#tool nuget:?package=Watson&version=7.0.14Install as a Cake Tool
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 | 👁 NuGet Version |
👁 NuGet |
This project is part of the .NET Foundation.
Watson 7 is a major consumer-facing release:
http.sysWebserverSettings.Protocols; HTTP/1.1, HTTP/2, and HTTP/3 supportTest.Automated and Test.XUnitRefer to for the full release history.
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:
437 req/s on HTTP/1.1 hello and 330 req/s on HTTP/1.1 json, while Watson 7 delivered 31,577 req/s and 36,021 req/s on the same scenarios.~25k req/s on hello and ~17k req/s on json, against a Kestrel reference point of roughly ~146k/~139k.~80k-95k req/s range on common HTTP/1.1 paths during longer benchmark runs, with corresponding latency improvements on the retained changes.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:
dotnet add package Watson
The Watson package contains everything needed for both server usage and extension development. The WatsonWebserver.Core namespace provides the shared abstractions, routing, and extensibility surface within the same package.
| Target | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| .NET Standard 2.1 | Yes | Yes | No (QUIC unavailable) |
| .NET 8 | Yes | Yes | Yes (where QUIC runtime is available) |
| .NET 10 | Yes | Yes | Yes (where QUIC runtime is available) |
On netstandard2.1, HTTP/3 is gracefully disabled at startup. All other features — routing, API routes, middleware, WebSockets, OpenAPI, health checks, and HTTP/2 — work without restriction.
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/.
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.
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");
}
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);
});
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"}
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.
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();
});
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);
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."}
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.
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");
});
Watson 7 includes native server-side WebSocket support.
Current scope:
WebSocketSessionserver.Statistics counters include websocket payload bytes and active websocket connectionsExample:
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");
}
WebSocketSession exposes:
IdIsConnectedSubprotocolRemoteIpRemotePortRequestMetadataStatisticsReceiveAsync()ReadMessagesAsync()SendTextAsync()SendBinaryAsync()CloseAsync()WebSocket settings live under WebserverSettings.WebSockets.
Important settings:
EnableMaxMessageSizeReceiveBufferSizeCloseHandshakeTimeoutMsAllowClientSuppliedGuidClientGuidHeaderNameSupportedVersionsEnableHttp1EnableHttp2EnableHttp3Important defaults:
Enable = falseMaxMessageSize = 16777216ReceiveBufferSize = 65536CloseHandshakeTimeoutMs = 5000AllowClientSuppliedGuid = falseClientGuidHeaderName = "x-guid"SupportedVersions = ["13"]EnableHttp1 = trueEnableHttp2 = falseEnableHttp3 = falsews:// when SSL is disabledwss:// when SSL is enabledWebSocketSettings.EnableHttp2 and EnableHttp3 are present for future protocol expansion and do not enable HTTP/2 or HTTP/3 websocket runtime support todaySystem.Net.WebSockets.WebSocket is exposed publiclySee for the focused WebSocket API guide and for WatsonWebsocket migration guidance.
By default:
| 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 |
Watson validates protocol settings before startup:
EnableHttp2Cleartext = trueAltSvc.Enabled requires HTTP/3 to be enabledusing 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");
}
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");
}
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.
The primary configuration object is WebserverSettings.
Important areas for consumers:
Protocols
EnableHttp1EnableHttp2EnableHttp3EnableHttp2CleartextIdleTimeoutMsMaxConcurrentStreamsHttp2Http3AltSvc
EnabledAuthorityPortHttp3AlpnMaxAgeSecondsIO
StreamBufferSizeMaxRequestsReadTimeoutMsMaxIncomingHeadersSizeEnableKeepAliveMaxRequestBodySizeMaxHeaderCountSsl
EnableSslCertificatePfxCertificateFilePfxCertificatePasswordMutuallyAuthenticateAcceptInvalidAcertificatesHeaders
DefaultHeadersIncludeContentLengthAccessControlDebugUseMachineHostnameWebserverSettings 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;
The most important 7.0 consumption rule is this:
DataAsBytes, DataAsString, or ReadBodyAsync()ReadChunk() when you are explicitly handling HTTP/1.1 chunked transfer-encodingUse 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.
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.
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.
private static async Task GetHello(HttpContextBase ctx)
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "text/plain";
await ctx.Response.Send("Hello", ctx.Token);
}
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);
}
}
SendChunk() works across supported protocols, but the wire behavior depends on the protocol:
Transfer-Encoding: chunkedprivate 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);
}
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);
}
}
Watson routes requests in this order:
PreflightPreRoutingPreAuthentication
StaticContentParameterDynamicAuthenticateRequestPostAuthentication
StaticContentParameterDynamicDefaultPostRoutingusing 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");
}
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 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.
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 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 support is built in. No extra package is required beyond Watson.
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/swaggerserver.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")))));
Register reusable schemas under OpenApiSettings.Schemas. They are emitted at
components.schemas and can be referenced from any route schema via
OpenApiSchemaMetadata.CreateRef("Name").
server.UseOpenApi(openApi =>
{
openApi.Schemas["Cat"] = new OpenApiSchemaMetadata
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchemaMetadata>
{
["kind"] = new OpenApiSchemaMetadata { Type = "string", Enum = new List<object> { "cat" } },
["whiskers"] = OpenApiSchemaMetadata.String()
},
Required = new List<string> { "kind", "whiskers" }
};
openApi.Schemas["Dog"] = new OpenApiSchemaMetadata
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchemaMetadata>
{
["kind"] = new OpenApiSchemaMetadata { Type = "string", Enum = new List<object> { "dog" } },
["breed"] = OpenApiSchemaMetadata.String()
},
Required = new List<string> { "kind", "breed" }
};
});
oneOf and a discriminatorUse OpenApiSchemaMetadata.CreateOneOf(...) to describe a value that may be one
of several shapes, and WithDiscriminator(...) to declare which property
selects the active branch. This lets client generators produce true polymorphic
types instead of a flattened union.
OpenApiSchemaMetadata animalSchema = OpenApiSchemaMetadata
.CreateOneOf(
OpenApiSchemaMetadata.CreateRef("Cat"),
OpenApiSchemaMetadata.CreateRef("Dog"))
.WithDiscriminator("kind", new Dictionary<string, string>
{
["cat"] = "#/components/schemas/Cat",
["dog"] = "#/components/schemas/Dog"
});
server.Routes.PreAuthentication.Static.Add(
HttpMethod.GET,
"/api/animals/{id}",
GetAnimalHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Get an animal", "Animals")
.WithResponse(200, OpenApiResponseMetadata.Json("Animal", animalSchema)));
The generated schema emits oneOf with $ref branches plus a
discriminator block carrying propertyName and the optional mapping.
WebserverSettings.UseMachineHostname controls the host value Watson uses when composing response host metadata.
Hostname is * or +, Watson forces UseMachineHostname = trueUseMachineHostname = trueExample:
WebserverSettings settings = new WebserverSettings("localhost", 9000);
settings.UseMachineHostname = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.Start();
When you bind Watson to 127.0.0.1 or localhost, only the local machine can reach it.
To expose Watson externally:
0.0.0.0, *, or + as appropriateHost header matches the binding constraints that apply to your environmentOn Windows, netsh http show urlacl and netsh http add urlacl ... are commonly needed when binding outside localhost.
Settings.Ssl.Enable is true, configure SslCertificate or a PFX file before calling Start()IO.MaxRequestBodySize, Watson enforces it against declared request body sizesIO.MaxHeaderCount, Watson limits inbound header cardinalityIO.EnableKeepAlive can reduce connection churnSettings.Debug; wire a logger into server.Events.Logger if you want to receive those messagesRefer to src/Test.Docker and its companion documentation.
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 from Test.Shared without invoking Test.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 comparisonsRecommended 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 .
Thanks to the contributors who have helped improve Watson Webserver, and to @DamienDennehy for allowing use of the Watson.Core package name in NuGet:
Refer to .
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 netstandard2.1 is compatible. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
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. |
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.14 | 945 | 4/25/2026 |
| 7.0.13 | 228 | 4/23/2026 |
| 7.0.12 | 396 | 4/20/2026 |
| 7.0.11 | 467 | 4/7/2026 |
| 7.0.10 | 729 | 4/6/2026 |
| 7.0.9 | 319 | 4/3/2026 |
| 7.0.8 | 255 | 4/2/2026 |
| 7.0.7 | 538 | 4/2/2026 |
| 7.0.6 | 327 | 4/2/2026 |
| 7.0.5 | 237 | 4/2/2026 |
| 7.0.4 | 232 | 4/2/2026 |
| 7.0.3 | 202 | 4/2/2026 |
| 7.0.2 | 207 | 4/2/2026 |
| 7.0.1 | 213 | 4/2/2026 |
| 7.0.0 | 370 | 3/30/2026 |
| 6.6.0 | 2,839 | 3/22/2026 |
| 6.5.5 | 3,631 | 2/5/2026 |
| 6.5.3 | 266 | 2/5/2026 |
| 6.5.1 | 2,251 | 12/24/2025 |
| 6.5.0 | 546 | 12/21/2025 |
Fixed HTTP/1.1 disconnect detection so client disconnects raise abort and disconnect events correctly, avoid false response-sent telemetry, and added regression coverage for large-response disconnect handling.