Handler
Overview
In SimpleW, a handler is the core execution unit that processes an HTTP request and produces a response. Handlers are designed to be :
- Flexible: sync, async, Task, ValueTask, with or without return values
- Fast: compiled once into an optimized execution pipeline
In short
A handler is just a method, but SimpleW turns it into a full HTTP execution pipeline.
What is a Handler ?
A handler is a C# delegate or a controller method that is registered to handle an HTTP request. From SimpleW’s point of view, a handler is anything that can be executed by the router to process a request. There are two equivalent ways to define a handler :
- Delegate-based handlers (functional style)
- Controller methods decorated with Route Attributes (class-based style)
Both models are internally normalized and executed through the same handler pipeline.
Delegate-Based Handlers
A delegate-based handler is registered explicitly using Map, MapGet, MapPost.
Example :
server.MapGet("/api/hello", static () => {
return new { message = "Hello world" };
});At runtime:
- The router matches the request path and HTTP method
- The associated handler is executed
- The handler result (if any) is processed and converted into an HTTP response
Controller Methods Handlers
Methods declared in a class that inherits from Controller and decorated with a [Route] attribute are also handlers.
Example :
public class UserController : Controller {
[Route("GET", "/api/users/:id")]
public object GetUser(int id) {
return new { id };
}
}From a conceptual point of view:
A controller method with a [Route] attribute is a handler.
Internally :
- The method is discovered via reflection
- A handler executor is generated exactly like for a delegate
- Parameters are bound using the same rules (route, query, session)
- The return value is processed by the same HttpResultHandler
Controllers do not introduce a different execution model. They are a structured way to declare handlers, not a separate abstraction.
Supported Handler Signatures
SimpleW supports a wide range of handler signatures.
Parameters
A handler can declare parameters that are automatically resolved :
| Parameter source | Resolution rule |
|---|---|
HttpSession | Injected directly |
| Route parameters | Matched by name (:id, :name, etc.) |
| Query string | Matched by name |
| Optional parameters | Default value is used if missing |
Example :
server.MapGet("/api/user/:id", (int id, string? filter = null) => {
return new { id, filter };
});[Route("GET", "/api/user/:id")]
public object User(int id, string? filter = null) {
return new { id, filter };
}Resolution priority :
- Route values
- Query string
- Default parameter value
If a required parameter is missing, an exception is thrown.
Return Types
Handlers may return
No Result
server.MapGet("/ping", (HttpSession session) => {
_ = session.Response.Json(new { message = "pong !" }).SendAsync();
});[Route("GET", "/ping")]
public void Ping() {
_ = Session.Response.Json(new { message = "pong !" }).SendAsync();
}The request is considered handled, no automatic response is sent.
Synchronous Result
server.MapGet("/api/data", () => {
return new { message = "Hello World !" };
});[Route("GET", "/ping")]
public object Data() {
return new { message = "Hello World !" };
}The returned value is forwarded to the Handler Result Processor.
Async (Task / ValueTask)
server.MapGet("/async", async () => {
await Task.Delay(100);
return new { message = "Hello World !" };
});[Route("GET", "/async")]
public async ValueTask<object> Data() {
await Task.Delay(100);
return new { message = "Hello World !" };
}Supported forms: ValueTask, ValueTask<T>, Task, Task<T>.
CancellationToken (RequestAborted)
By default, a handler runs to completion even if the client disconnects. This behavior is acceptable in most cases, but sometimes you want to stop the handler immediately when the client goes away — especially for handlers that perform I/O or long-running work.
SimpleW exposes a CancellationToken directly from HttpSession, allowing a handler to cooperatively stop its execution when :
- the client closes the connection,
- a network write/read fails,
- the session is closed by the server (timeout, dispose, etc.).
The token is available through :
session.RequestAbortedConcept
The CancellationToken does not automatically kill your code. Cancellation is cooperative and must be explicitly honored by your logic :
- pass the token to APIs that support cancellation (database, HTTP client, delays, etc.)
- or manually check the token inside long-running loops.
Example : cancellable Delay
server.MapGet("/delay", async (HttpSession session) => {
try {
Console.WriteLine($"[START] session={session.Id} t={Environment.TickCount64}");
await Task.Delay(TimeSpan.FromSeconds(30), session.RequestAborted);
Console.WriteLine($"[END] session={session.Id} t={Environment.TickCount64}");
}
catch (OperationCanceledException) when (session.RequestAborted.IsCancellationRequested) {
Console.WriteLine($"[OperationCanceledException] session={session.Id} t={Environment.TickCount64}");
}
catch (Exception ex) {
Console.WriteLine($"[Exception] session={session.Id} t={Environment.TickCount64}");
}
return new { message = "Hello World!" };
});[Route("GET", "/delay")]
public async ValueTask<object> Delay() {
try {
Console.WriteLine($"[START] session={Session.Id} t={Environment.TickCount64}");
await Task.Delay(TimeSpan.FromSeconds(30), Session.RequestAborted);
Console.WriteLine($"[END] session={Session.Id} t={Environment.TickCount64}");
}
catch (OperationCanceledException) when (Session.RequestAborted.IsCancellationRequested) {
Console.WriteLine($"[OperationCanceledException] session={Session.Id} t={Environment.TickCount64}");
}
catch (Exception ex) {
Console.WriteLine($"[Exception] session={Session.Id} t={Environment.TickCount64}");
}
return new { message = "Hello World!" };
}How to test :
- Open the URL in a browser.
- Wait 30 seconds to let the handler complete normally.
- Reload the page, then close the connection before 30 seconds.
The handler will be canceled immediately when the client disconnects.
Example : cancellable PostgreSQL query
server.MapGet("/execute", async (HttpSession session) => {
await using var conn = new Npgsql.NpgsqlConnection(connString);
await conn.OpenAsync(session.RequestAborted);
await using var cmd = new Npgsql.NpgsqlCommand("select pg_sleep(1800);", conn);
await cmd.ExecuteNonQueryAsync(session.RequestAborted);
return "OK";
});[Route("GET", "/execute")]
public async ValueTask<object> Execute() {
await using var conn = new Npgsql.NpgsqlConnection(connString);
await conn.OpenAsync(Session.RequestAborted);
await using var cmd = new Npgsql.NpgsqlCommand("select pg_sleep(1800);", conn);
await cmd.ExecuteNonQueryAsync(Session.RequestAborted);
return "OK";
}If the client disconnects while the query is running, it will be automatically cancelled.
Example: CPU loop with cooperative cancellation
server.MapGet("/work", (HttpSession session) => {
for (int i = 0; i < 10_000_000; i++) {
session.ThrowIfAborted(); // stops if client is gone
DoWork(i);
}
return "DONE";
});[Route("GET", "/work")]
public object Work() {
for (int i = 0; i < 10_000_000; i++) {
Session.ThrowIfAborted(); // stops if client is gone
DoWork(i);
}
return "DONE";
}Manual state check
if (session.RequestAborted.IsCancellationRequested) {
return;
}This allows exiting gracefully without throwing an exception.
Best practices
- Prefer
asynchandlers. - Always use async APIs when available.
- Always pass
session.RequestAbortedto long-running I/O operations. - Use
ThrowIfAborted()for CPU-bound loops or custom processing.
TIP
If the work should continue even after the HTTP response is sent, use the Background service instead. It lets the handler return quickly, typically with 202 Accepted, while the task continues in a background worker.
Direct Response Control
A handler may directly manipulate the response :
server.MapGet("/raw", (HttpSession session) => {
return session.Response
.Status(200)
.Text("Hello");
});[Route("GET", "/raw")]
public object Raw() {
return Session.Response
.Status(200)
.Text("Hello");
}If an HttpResponse is returned, SimpleW ensures it belongs to the current session.
NOTE
See the Response for more examples.
Handler Result Processing
When a handler returns a non-null value, it is passed to a global HttpResultHandler delegate.
Default Behavior
By default :
- The result is serialized as JSON
- The response is sent immediately
Conceptually :
handler() -> object result -> ResultHandler -> HttpResponseCustom Handler Result
You can override this behavior globally :
server.ConfigureResultHandler((session, result) => {
Console.WriteLine("Processing result");
return session.Response.Json(result).SendAsync();
});This allows logging, delayed responses, conditional serialization, custom response strategies...
NOTE
See the Result Handler for more examples.
Middleware and Handlers
Handlers are executed inside a middleware pipeline.
Execution order:
- First middleware
- Next middleware
- ...
- Final handler
- Unwind middleware stack
Example middleware :
server.UseMiddleware(async (session, next) => {
Console.WriteLine("Before handler");
await next();
Console.WriteLine("After handler");
});From the handler perspective, middleware is transparent.
NOTE
See the Middleware for more examples.