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 };
});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", static () => { });The request is considered handled, no automatic response is sent.
Synchronous Result
server.MapGet("/api/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 "done";
});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!" };
});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";
});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";
});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.
Direct Response Control
A handler may directly manipulate the response :
server.MapGet("/raw", (HttpSession session) => {
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.
