Watson 7.0.0

dotnet add package Watson --version 7.0.0
                    
NuGet\Install-Package Watson -Version 7.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Watson" Version="7.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Watson" Version="7.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Watson" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Watson --version 7.0.0
                    
#r "nuget: Watson, 7.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Watson@7.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Watson&version=7.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Watson&version=7.0.0
                    
Install as a Cake Tool

Watson

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 NuGet Version NuGet
Watson.Core NuGet Version NuGet

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.Automated and Test.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/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.
  • 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/s on hello and ~17k req/s on json, against a Kestrel reference point of roughly ~146k/~139k.
  • Across the 7.0 optimizations, Watson 7 repeatedly moved into the ~80k-95k req/s range 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:

  • Id
  • IsConnected
  • Subprotocol
  • RemoteIp
  • RemotePort
  • Request
  • Metadata
  • Statistics
  • ReceiveAsync()
  • ReadMessagesAsync()
  • SendTextAsync()
  • SendBinaryAsync()
  • CloseAsync()

Statistics

Watson exposes counters at two levels:

  • WebSocketSession.Statistics for per-session message and byte counts
  • server.Statistics for 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:

  • Enable
  • MaxMessageSize
  • ReceiveBufferSize
  • CloseHandshakeTimeoutMs
  • AllowClientSuppliedGuid
  • ClientGuidHeaderName
  • SupportedVersions
  • EnableHttp1

Important defaults:

  • Enable = false
  • MaxMessageSize = 16777216
  • ReceiveBufferSize = 65536
  • CloseHandshakeTimeoutMs = 5000
  • AllowClientSuppliedGuid = false
  • ClientGuidHeaderName = "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.WebSocket is 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.Enabled requires 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:

  • Protocols
    • EnableHttp1
    • EnableHttp2
    • EnableHttp3
    • EnableHttp2Cleartext
    • IdleTimeoutMs
    • MaxConcurrentStreams
    • Http2
    • Http3
  • AltSvc
    • Enabled
    • Authority
    • Port
    • Http3Alpn
    • MaxAgeSeconds
  • IO
    • StreamBufferSize
    • MaxRequests
    • ReadTimeoutMs
    • MaxIncomingHeadersSize
    • EnableKeepAlive
    • MaxRequestBodySize
    • MaxHeaderCount
  • Ssl
    • Enable
    • SslCertificate
    • PfxCertificateFile
    • PfxCertificatePassword
    • MutuallyAuthenticate
    • AcceptInvalidAcertificates
  • Headers
    • DefaultHeaders
    • IncludeContentLength
  • AccessControl
  • Debug
  • UseMachineHostname

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, or ReadBodyAsync()
  • Only use ReadChunk() when you are explicitly handling HTTP/1.1 chunked transfer-encoding

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:

  • Preflight
  • PreRouting
  • PreAuthentication
    • Static
    • Content
    • Parameter
    • Dynamic
  • AuthenticateRequest
  • PostAuthentication
    • Static
    • Content
    • Parameter
    • Dynamic
  • Default
  • PostRouting

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 Hostname is * or +, Watson forces UseMachineHostname = 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 Host header 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.Enable is true, configure SslCertificate or a PFX file before calling Start()
  • 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.EnableKeepAlive can reduce connection churn
  • Debug logging options live under Settings.Debug; wire a logger into server.Events.Logger if 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 server
  • src/Test.XUnit: CI-oriented test runner that executes shared test logic from Test.Shared without invoking Test.Automated
  • src/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

Watson 7.0 foundation: shared semantic pipeline extraction, protocol configuration surface, lifecycle metadata, and fail-fast protocol validation