SkillAgentSearch skills...

WatsonWebserver

Watson is the fastest, easiest way to build scalable RESTful web servers and services in C#.

Install / Use

/learn @dotnet/WatsonWebserver

README

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 through SwiftStack integration, provding a native first-class API route experience with automatic JSON serialization, typed parameter access, middleware, structured authentication, request timeouts, and health checks
  • 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
  • Native WebSocket support including hybrid REST/WebSocket routes
  • 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

View on GitHub
GitHub Stars479
CategoryDevelopment
Updated2d ago
Forks100

Languages

C#

Security Score

100/100

Audited on Mar 30, 2026

No findings