![]() |
VOOZH | about |
dotnet add package Watson.Lite --version 6.6.0
NuGet\Install-Package Watson.Lite -Version 6.6.0
<PackageReference Include="Watson.Lite" Version="6.6.0" />
<PackageVersion Include="Watson.Lite" Version="6.6.0" />Directory.Packages.props
<PackageReference Include="Watson.Lite" />Project file
paket add Watson.Lite --version 6.6.0
#r "nuget: Watson.Lite, 6.6.0"
#:package Watson.Lite@6.6.0
#addin nuget:?package=Watson.Lite&version=6.6.0Install as a Cake Addin
#tool nuget:?package=Watson.Lite&version=6.6.0Install as a Cake Tool
Simple, scalable, fast, async web server for processing RESTful HTTP/HTTPS requests, written in C#.
| Package | NuGet Version | Downloads |
|---|---|---|
| Watson | 👁 NuGet Version |
👁 NuGet |
| Watson.Lite | 👁 NuGet Version |
👁 NuGet |
| Watson.Core | 👁 NuGet Version |
👁 NuGet |
Special thanks to @DamienDennehy for allowing us the use of the Watson.Core package name in NuGet!
This project is part of the .NET Foundation along with other projects like the .NET Runtime.
MaxRequestBodySize and MaxHeaderCount settingsPostRouting now properly awaited, per-instance CancellationTokenSourceIDisposable on context/request/response base classes, fixed MemoryStream leaksMemoryStream/StringBuilder in Lite body reading, cached query elements, ReaderWriterLockSlim in route managersI'd like to extend a special thanks to those that have helped make Watson Webserver better.
Watson is a webserver that operates on top of the underlying http.sys within the operating system. Watson.Lite was created by merging HttpServerLite. Watson.Lite does not have a dependency on http.sys, and is implemented using a TCP implementation provided by CavemanTcp.
The dependency on http.sys (or lack thereof) creates subtle differences between the two libraries, however, the configuration and management of each should be consistent.
Watson.Lite is generally less performant than Watson, because the HTTP implementation is in user space.
127.0.0.1 or localhostX509Certificate2 must be suppliedWatson and Watson.Lite always routes in the following order (configure using Webserver.Routes):
.Preflight - handling preflight requests (generally with HTTP method OPTIONS).PreRouting - always invoked before any routing determination is made.PreAuthentication - a routing group, comprised of:
.Static - static routes, e.g. an HTTP method and an explicit URL.Content - file serving routes, e.g. a directory where files can be read.Parameter - routes where variables are specified in the path, e.g. /user/{id}.Dynamic - routes where the URL is defined by a regular expression.AuthenticateRequest - demarcation route between unauthenticated and authenticated routes.PostAuthentication - a routing group with a structure identical to .PreAuthentication.Default - the default route; all requests go here if not routed previously.PostRouting - always invoked, generally for logging and telemetryIf you do not wish to use authentication, you should map your routes in the .PreAuthentication routing group (though technically they can be placed in .PostAuthentication or .Default assuming the AuthenticateRequest method is null.
As a general rule, never try to send data to an HttpResponse while in the .PostRouting route. If a response has already been sent, the attempt inside of .PostRouting will fail.
It is recommended that you implement authentication in .AuthenticateRequest. Should a request fail authentication, return a response within that route. The HttpContextBase class has properties that can hold authentication-related or session-related metadata, specifically, .Metadata.
By default, Watson and Watson.Lite will permit all inbound connections.
Server.AccessControl.DenyList.Add(ip, netmask)Server.AccessControl.Mode = AccessControlMode.DefaultDenyServer.AccessControl.PermitList.Add(ip, netmask)Refer to Test.Default for a full example.
using System.IO;
using System.Text;
using WatsonWebserver;
static void Main(string[] args)
{
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
WebserverBase server = new Webserver(settings, DefaultRoute);
server.Start();
Console.ReadLine();
}
static async Task DefaultRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the default route!");
Then, open your browser to http://127.0.0.1:9000/.
Refer to Test.Routing for a full example.
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using WatsonWebserver;
static void Main(string[] args)
{
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
WebserverBase server = new Webserver(settings, DefaultRoute);
// add content routes
server.Routes.PreAuthentication.Content.Add("/html/", true);
server.Routes.PreAuthentication.Content.Add("/img/watson.jpg", false);
// add static routes
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/hello/", GetHelloRoute);
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/howdy/", async (HttpContextBase ctx) =>
{
await ctx.Response.Send("Hello from the GET /howdy static route!");
return;
});
// add parameter routes
server.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/{version}/bar", GetBarRoute);
// add dynamic routes
server.Routes.PreAuthentication.Dynamic.Add(HttpMethod.GET, new Regex("^/foo/\\d+$"), GetFooWithId);
server.Routes.PreAuthentication.Dynamic.Add(HttpMethod.GET, new Regex("^/foo/?$"), GetFoo);
// start the server
server.Start();
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
static async Task GetHelloRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the GET /hello static route!");
static async Task GetBarRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the GET /" + ctx.Request.Url.Parameters["version"] + "/bar route!");
static async Task GetFooWithId(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the GET /foo/[id] dynamic route!");
static async Task GetFoo(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the GET /foo/ dynamic route!");
static async Task DefaultRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the default route!");
server.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/hello/", GetHelloRoute, MyExceptionRoute);
static async Task GetHelloRoute(HttpContextBase ctx) => throw new Exception("Whoops!");
static async Task MyExceptionRoute(HttpContextBase ctx, Exception e)
{
ctx.Response.StatusCode = 500;
await ctx.Response.Send(e.Message);
}
Webserver server = new Webserver("127.0.0.1", 9000, false, DefaultRoute);
// set default permit (permit any) with deny list to block specific IP addresses or networks
server.Settings.AccessControl.Mode = AccessControlMode.DefaultPermit;
server.Settings.AccessControl.DenyList.Add("127.0.0.1", "255.255.255.255");
// set default deny (deny all) with permit list to permit specific IP addresses or networks
server.Settings.AccessControl.Mode = AccessControlMode.DefaultDeny;
server.Settings.AccessControl.PermitList.Add("127.0.0.1", "255.255.255.255");
Watson supports both receiving chunked data and sending chunked data (indicated by the header Transfer-Encoding: chunked).
Refer to Test.ChunkServer for a sample implementation.
static async Task UploadData(HttpContextBase ctx)
{
if (ctx.Request.ChunkedTransfer)
{
bool finalChunk = false;
while (!finalChunk)
{
Chunk chunk = await ctx.Request.ReadChunk();
// work with chunk.Length and chunk.Data (byte[])
finalChunk = chunk.IsFinalChunk;
}
}
else
{
// read from ctx.Request.Data stream
}
}
static async Task DownloadChunkedFile(HttpContextBase ctx)
{
using (FileStream fs = new FileStream("./img/watson.jpg", , FileMode.Open, FileAccess.Read))
{
ctx.Response.StatusCode = 200;
ctx.Response.ChunkedTransfer = true;
byte[] buffer = new byte[4096];
while (true)
{
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
byte[] data = new byte[bytesRead];
Buffer.BlockCopy(buffer, 0, bytesRead, data, 0); // only copy the read data
if (bytesRead > 0)
{
await ctx.Response.SendChunk(data, false);
}
else
{
await ctx.Response.SendChunk(Array.Empty<byte>(), true);
break;
}
}
}
return;
}
Watson supports sending server-sent events. Refer to Test.ServerSentEvents for a sample implementation. The SendEvent method handles formatting ServerSentEvent objects for transmission to the client.
static async Task SendEvents(HttpContextBase ctx)
{
ctx.Response.StatusCode = 200;
ctx.Response.ServerSentEvents = true;
for (int i = 1; i <= 10; i++)
{
ServerSentEvent ev = new ServerSentEvent
{
Id = i.ToString(),
Event = "my-event-type",
Data = $"Event number {i.ToString()}",
};
bool isFinal = (i == 10);
await ctx.Response.SendEvent(ev, isFinal);
}
return;
}
HostBuilder helps you set up your server much more easily by introducing a chain of settings and routes instead of using the server class directly. Special thanks to @sapurtcomputer30 for producing this fine feature!
Refer to Test.HostBuilder for a full sample implementation.
using WatsonWebserver.Extensions.HostBuilderExtension;
Webserver server = new HostBuilder("127.0.0.1", 8000, false, DefaultRoute)
.MapStaticRoute(HttpMethod.GET, GetUrlsRoute, "/links")
.MapStaticRoute(HttpMethod.POST, CheckLoginRoute, "/login")
.MapStaticRoute(HttpMethod.POST, TestRoute, "/test")
.Build();
server.Start();
Console.WriteLine("Server started");
Console.ReadKey();
static async Task DefaultRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from default route!");
static async Task GetUrlsRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Here are your links!");
static async Task CheckLoginRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Checking your login!");
static async Task TestRoute(HttpContextBase ctx) =>
await ctx.Response.Send("Hello from the test route!");
Watson and Watson.Lite include built-in OpenAPI 3.0 documentation generation and Swagger UI. No additional packages are required - OpenAPI support is included in WatsonWebserver.Core.
Refer to Test.OpenApi for a complete working example.
using WatsonWebserver;
using WatsonWebserver.Core;
using WatsonWebserver.Core.OpenApi;
WebserverSettings settings = new WebserverSettings("localhost", 8080);
Webserver server = new Webserver(settings, DefaultRoute);
// Enable OpenAPI with Swagger UI
server.UseOpenApi(openApi =>
{
openApi.Info.Title = "My API";
openApi.Info.Version = "1.0.0";
openApi.Info.Description = "My API description";
});
server.Start();
After starting the server:
http://localhost:8080/openapi.jsonhttp://localhost:8080/swaggerBy default, both OpenAPI and Swagger UI are enabled when you call UseOpenApi(). You can control this behavior using the EnableOpenApi and EnableSwaggerUi settings:
// Disable OpenAPI entirely (no endpoints registered)
server.UseOpenApi(openApi =>
{
openApi.EnableOpenApi = false;
});
// Enable OpenAPI JSON but disable Swagger UI
server.UseOpenApi(openApi =>
{
openApi.Info.Title = "My API";
openApi.Info.Version = "1.0.0";
openApi.EnableOpenApi = true; // Default: true
openApi.EnableSwaggerUi = false; // Disables /swagger endpoint
});
// Enable both (default behavior)
server.UseOpenApi(openApi =>
{
openApi.Info.Title = "My API";
openApi.Info.Version = "1.0.0";
openApi.EnableOpenApi = true; // Default: true
openApi.EnableSwaggerUi = true; // Default: true
});
| Setting | Default | Description |
|---|---|---|
EnableOpenApi |
true |
When false, no OpenAPI endpoints are registered (both /openapi.json and /swagger are disabled) |
EnableSwaggerUi |
true |
When false, only the Swagger UI (/swagger) is disabled; OpenAPI JSON remains available |
Add OpenAPI documentation to routes using the openApiMetadata parameter. The fluent API makes it easy to build complete documentation:
using WatsonWebserver.Core.OpenApi;
// GET route with query parameters
server.Routes.PreAuthentication.Static.Add(
HttpMethod.GET,
"/api/users",
GetUsersHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Get all users", "Users")
.WithDescription("Returns a list of all users, optionally filtered by status")
.WithParameter(OpenApiParameterMetadata.Query("active", "Filter by active status", false, OpenApiSchemaMetadata.Boolean()))
.WithParameter(OpenApiParameterMetadata.Query("limit", "Maximum number of results", false, OpenApiSchemaMetadata.Integer()))
.WithResponse(200, OpenApiResponseMetadata.Json(
"List of users",
OpenApiSchemaMetadata.CreateArray(OpenApiSchemaMetadata.CreateRef("User")))));
// GET route with path parameter
server.Routes.PreAuthentication.Parameter.Add(
HttpMethod.GET,
"/api/users/{id}",
GetUserByIdHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Get user by ID", "Users")
.WithDescription("Retrieves a specific user by their unique identifier")
.WithParameter(OpenApiParameterMetadata.Path("id", "User ID", OpenApiSchemaMetadata.Integer()))
.WithResponse(200, OpenApiResponseMetadata.Json("User found", OpenApiSchemaMetadata.CreateRef("User")))
.WithResponse(404, OpenApiResponseMetadata.NotFound()));
// POST route with request body
server.Routes.PreAuthentication.Static.Add(
HttpMethod.POST,
"/api/users",
CreateUserHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Create user", "Users")
.WithDescription("Creates a new user in the system")
.WithRequestBody(OpenApiRequestBodyMetadata.Json(
new OpenApiSchemaMetadata
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchemaMetadata>
{
["name"] = OpenApiSchemaMetadata.String(),
["email"] = OpenApiSchemaMetadata.String("email"),
["age"] = OpenApiSchemaMetadata.Integer()
},
Required = new List<string> { "name", "email" }
},
"User data to create",
required: true))
.WithResponse(201, OpenApiResponseMetadata.Created(OpenApiSchemaMetadata.CreateRef("User")))
.WithResponse(400, OpenApiResponseMetadata.BadRequest()));
// DELETE route
server.Routes.PreAuthentication.Parameter.Add(
HttpMethod.DELETE,
"/api/users/{id}",
DeleteUserHandler,
openApiMetadata: OpenApiRouteMetadata.Create("Delete user", "Users")
.WithParameter(OpenApiParameterMetadata.Path("id", "User ID", OpenApiSchemaMetadata.Integer()))
.WithResponse(204, OpenApiResponseMetadata.NoContent())
.WithResponse(404, OpenApiResponseMetadata.NotFound()));
server.UseOpenApi(openApi =>
{
// API Information (required)
openApi.Info.Title = "My API";
openApi.Info.Version = "1.0.0";
openApi.Info.Description = "API description";
openApi.Info.Contact = new OpenApiContact
{
Name = "Support",
Email = "support@example.com",
Url = "https://example.com/support"
};
openApi.Info.License = new OpenApiLicense
{
Name = "MIT",
Url = "https://opensource.org/licenses/MIT"
};
// Tags for grouping endpoints in Swagger UI
openApi.Tags.Add(new OpenApiTag { Name = "Users", Description = "User management operations" });
openApi.Tags.Add(new OpenApiTag { Name = "Products", Description = "Product catalog operations" });
// Security schemes (for authenticated APIs)
openApi.SecuritySchemes["ApiKey"] = new OpenApiSecurityScheme
{
Type = "apiKey",
Name = "X-API-Key",
In = "header",
Description = "API key for authorization"
};
// Or use Bearer token authentication
openApi.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = "http",
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT authorization header"
};
// Enable/disable OpenAPI and Swagger
openApi.EnableOpenApi = true; // Default (set to false to disable all OpenAPI endpoints)
openApi.EnableSwaggerUi = true; // Default (set to false to disable only Swagger UI)
// Customize endpoint paths
openApi.DocumentPath = "/openapi.json"; // Default
openApi.SwaggerUiPath = "/swagger"; // Default
// Control which routes are documented
openApi.IncludePreAuthRoutes = true; // Default
openApi.IncludePostAuthRoutes = true; // Default
openApi.IncludeContentRoutes = false; // Default (file-serving routes)
});
The OpenApiSchemaMetadata class provides convenient factory methods for common types:
OpenApiSchemaMetadata.String() // string
OpenApiSchemaMetadata.String("email") // string with format (email, date-time, uri, etc.)
OpenApiSchemaMetadata.Integer() // integer (int32)
OpenApiSchemaMetadata.Long() // integer (int64)
OpenApiSchemaMetadata.Number() // number (double)
OpenApiSchemaMetadata.Boolean() // boolean
OpenApiSchemaMetadata.CreateArray(items) // array of items
OpenApiSchemaMetadata.CreateRef("User") // $ref to #/components/schemas/User
The OpenApiResponseMetadata class provides factory methods for common responses:
OpenApiResponseMetadata.Json("Description", schema) // 200 with JSON body
OpenApiResponseMetadata.Created(schema) // 201 Created
OpenApiResponseMetadata.NoContent() // 204 No Content
OpenApiResponseMetadata.BadRequest() // 400 Bad Request
OpenApiResponseMetadata.Unauthorized() // 401 Unauthorized
OpenApiResponseMetadata.Forbidden() // 403 Forbidden
OpenApiResponseMetadata.NotFound() // 404 Not Found
To correctly handle the Host HTTP header, a new boolean property, UseMachineHostname, has been introduced in WebserverSettings. This is especially important when binding to all network interfaces.
All-Interface Binding Behavior: When Hostname is set to 0.0.0.0 (or */+ for Watson only), the server will mandatorily use the machine's actual hostname for the Host header in HTTP responses. This prevents UriFormatException on modern .NET runtimes. In this scenario, UseMachineHostname is forced to true.
Default Behavior: For any other hostname (e.g., localhost or a specific IP address), this feature is disabled by default. The Host header will use the value specified in the Hostname setting.
Manual Activation: You can force the use of the machine's hostname for any binding by setting UseMachineHostname = true in the settings.
Note: Watson.Lite does not support
*or+as hostname values. Use0.0.0.0to listen on all interfaces.
Example 1: Binding to All Interfaces
// The server detects 0.0.0.0 and uses the machine's hostname for the Host header.
// Use 0.0.0.0 for Watson.Lite; Watson also supports "*" or "+".
var server = new Server("0.0.0.0", 9000, false, DefaultRoute);
server.Start();
Example 2: Manually Enabling for a Specific Hostname
// By default, the Host header would be "localhost:9000".
// By setting UseMachineHostname = true, we force it to use the machine's actual hostname.
var settings = new WebserverSettings("localhost", 9000);
settings.UseMachineHostname = true;
var server = new Server(settings, DefaultRoute);
server.Start();
When UseMachineHostname is active, the retrieved hostname will vary depending on the operating system and network configuration. Here are some typical examples (after sanitization):
desktop-a1b2c3dmarcos-macbook-pro.localubuntu-serverpixel-7-promarcos-iphone.localWhen you configure Watson to listen on 127.0.0.1 or localhost, it will only respond to requests received from within the local machine.
To configure access from other nodes outside of localhost, use the following:
*, +, or 0.0.0.0. You MUST run as administrator (operating system limitation)netsh command:
netsh http show urlaclnetsh http add urlacl url=http://[hostname]:[port]/ user=everyone listen=yeshostname and port are the values you are using in the constructorWhen you configure Watson.Lite to listen on 127.0.0.1, it will only respond to requests received from within the local machine.
To configure access from other nodes outside of the local machine, use the following:
0.0.0.0. You MUST run as administrator (operating system limitation)* or + as hostname values; use 0.0.0.0 insteadX509Certificate2 objectPlease refer to the Test.Docker project and the Docker.md file therein.
Refer to CHANGELOG.md for version history.
| 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 | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 is compatible. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 is compatible. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. 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.Lite:
| Package | Downloads |
|---|---|
|
VpnHood.AppLib.WebServer
Tiny internal webserver to server your single-page application (SPA). You need this only if you want to create a UI for your VpnHood client by single-page application (SPA) |
|
|
IgniteView.Core
A Lightweight framework for building cross-platform web apps with .NET |
|
|
tgv-watson-server
Package Description |
|
|
EASYTools.S3Server
Emulated Amazon Web Services (AWS) Simple Storage Service (S3) server-side interface. |
|
|
Watson.Extensions.Hosting.Samples.Default
Package Description |
Showing the top 4 popular GitHub repositories that depend on Watson.Lite:
| Repository | Stars |
|---|---|
|
vpnhood/VpnHood
Undetectable, Fast, Cross-Platform, Free VPN
|
|
|
Taiizor/Sucrose
Sucrose is a versatile wallpaper engine that brings life to your desktop with a wide range of interactive wallpapers.
|
|
| PluralKit/PluralKit | |
| UmamusumeResponseAnalyzer/UmamusumeResponseAnalyzer |
| Version | Downloads | Last Updated |
|---|---|---|
| 6.6.0 | 1,573 | 3/22/2026 |
| 6.5.6 | 992 | 2/6/2026 |
| 6.5.5 | 136 | 2/5/2026 |
| 6.5.3 | 134 | 2/5/2026 |
| 6.5.2 | 739 | 12/28/2025 |
| 6.5.1 | 282 | 12/24/2025 |
| 6.5.0 | 184 | 12/21/2025 |
| 6.4.0 | 985 | 10/17/2025 |
| 6.3.17 | 347 | 10/13/2025 |
| 6.3.15 | 498 | 9/19/2025 |
| 6.3.13 | 332 | 9/4/2025 |
| 6.3.12 | 1,438 | 8/6/2025 |
| 6.3.10 | 858 | 6/9/2025 |
| 6.3.9 | 1,923 | 4/1/2025 |
| 6.3.8 | 302 | 4/1/2025 |
| 6.3.7 | 230 | 3/29/2025 |
| 6.3.6 | 1,588 | 1/22/2025 |
| 6.3.5 | 862 | 1/20/2025 |
| 6.3.4 | 294 | 1/7/2025 |
| 6.3.3 | 284 | 1/2/2025 |
Stability, security, and performance improvements; IDisposable on context/request/response; directory traversal protection; request body size limits; route manager concurrency improvements; routing pipeline deduplication