![]() |
VOOZH | about |
dotnet add package Watson.Core --version 7.0.3
NuGet\Install-Package Watson.Core -Version 7.0.3
<PackageReference Include="Watson.Core" Version="7.0.3" />
<PackageVersion Include="Watson.Core" Version="7.0.3" />Directory.Packages.props
<PackageReference Include="Watson.Core" />Project file
paket add Watson.Core --version 7.0.3
#r "nuget: Watson.Core, 7.0.3"
#:package Watson.Core@7.0.3
#addin nuget:?package=Watson.Core&version=7.0.3Install as a Cake Addin
#tool nuget:?package=Watson.Core&version=7.0.3Install 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 |
| Watson.Core | 👁 NuGet Version |
👁 NuGet |
Special thanks to @DamienDennehy for allowing use of the Watson.Core package name in 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
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
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 or Watson.Core.
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")))));
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:
Refer to .
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
Showing the top 3 NuGet packages that depend on Watson.Core:
| Package | Downloads |
|---|---|
|
Watson.Lite
Simple, fast, async C# web server for handling REST requests with SSL support, targeted to .NET Core, .NET Standard, and .NET Framework. Watson.Lite has no dependency on http.sys. |
|
|
Squid.Prism.Server.Core
SquidPrism Engine is a flexible voxel-based MMO engine built in C#. Features chunk-based world management, efficient networking for multiplayer environments, and a modular architecture. |
|
|
Watson.Extensions.Hosting
Brings the power and flexibility of Microsoft.Extensions.Hosting (the .NET Generic Host) to the lightweight and high-performance WatsonWebserver. Build modern web servers with Dependency Injection, Middleware, and ASP.NET Core-style Controllers and Minimal APIs. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.0.3 | 240 | 4/2/2026 |
| 7.0.2 | 124 | 4/2/2026 |
| 7.0.1 | 166 | 4/2/2026 |
| 7.0.0 | 313 | 3/30/2026 |
| 6.6.0 | 3,894 | 3/22/2026 |
| 6.5.6 | 2,891 | 2/6/2026 |
| 6.5.5 | 1,468 | 2/5/2026 |
| 6.5.3 | 261 | 2/5/2026 |
| 6.5.1 | 2,803 | 12/24/2025 |
| 6.5.0 | 557 | 12/21/2025 |
| 6.4.0 | 6,582 | 10/17/2025 |
| 6.3.17 | 4,641 | 10/13/2025 |
| 6.3.15 | 4,121 | 9/19/2025 |
| 6.3.13 | 2,463 | 9/4/2025 |
| 6.3.12 | 4,013 | 8/6/2025 |
| 6.3.10 | 2,937 | 6/9/2025 |
| 6.3.9 | 5,021 | 4/1/2025 |
| 6.3.7 | 461 | 4/1/2025 |
| 6.3.6 | 741 | 3/29/2025 |
| 6.3.5 | 13,290 | 1/20/2025 |
Watson 7.0 foundation: shared semantic pipeline extraction, protocol configuration surface, lifecycle metadata, and fail-fast protocol validation