Server
SimpleWServer is the main entry point to host a SimpleW Web server. You can use it in a procedural style (step-by-step calls) or in a fluent style (chaining calls).
Lifecycle actions
| Method | Behavior |
|---|---|
StartAsync(ct) | Starts listening and completes once startup succeeds, without waiting for the server lifetime. |
RunAsync(ct) | Starts the server and blocks until the server is stopped / cancelled. |
StopAsync() | Stops listening, closes sessions, disposes internal resources. |
ReloadListenerAsync(reconfigure, ct) | Hot-reloads the listener (endpoint / TLS, etc.) while the server is running. |
Lifecycle state
SimpleWServer.State is a thread-safe snapshot represented by SimpleWServerState:
| State | Meaning |
|---|---|
Stopped | Not running; pre-start configuration is allowed. |
Starting | The engine is starting. |
Started | Running normally. |
Reloading | Replacing or restoring the listener. |
Stopping | Stopping the engine and cleaning local resources. |
Faulted | Startup cleanup, listener restoration, or engine shutdown failed. |
The normal transitions are:
Stopped -> Starting -> Started
Started -> Reloading -> Started
Started -> Stopping -> StoppedAll lifecycle operations use the same synchronization point. Concurrent calls wait for the transition in progress and then apply their intent. For example, two starts only start the engine once, a stop during startup waits and then stops, and a start during shutdown waits and then starts a new generation.
StartAsync() and ReloadListenerAsync() reject a server in Faulted. Call StopAsync() to retry cleanup; after it succeeds, State is Stopped and the server can be started again.
Instantiation
You can create a server from an IPAddress + port or from an EndPoint.
using System.Net;
using SimpleW;
var server1 = new SimpleWServer(IPAddress.Loopback, 8080);
var server2 = new SimpleWServer(new IPEndPoint(IPAddress.Any, 8081));Configuration
Configure(Action<SimpleWSServerOptions>) lets you customize server options.
Important: calling
Configure(...)during any lifecycle transition or while the server is running throws an exception.
server.Configure(options => {
options.MaxRequestBodySize = 100 * 1024 * 1024; // 100Mo
options.SessionTimeout = TimeSpan.FromSeconds(30);
});Options are automatically validated/normalized when starting (StartAsync calls Options.ValidateAndNormalize()).
INFO
See all options.
For advanced scenarios, see how to replace the core components used by SimpleW.
Lifecycle state callbacks
You can observe every server state transition with OnStateChanged:
OnStateChanged(Action<SimpleWServer, SimpleWServerState>)OnStateChanged(Func<SimpleWServer, SimpleWServerState, Task>)
Subscribing does not emit the initial Stopped state. Callbacks run sequentially in registration order, and asynchronous callbacks are awaited before the transition completes. Callback exceptions are logged without interrupting the lifecycle or the remaining subscribers.
Lifecycle methods must not be called from these callbacks; such reentrant calls are rejected to prevent a deadlock.
var server = new SimpleWServer(IPAddress.Any, 8080)
.OnStateChanged((s, state) => Console.WriteLine($"Server {s.Port} state: {state}"));
await server.RunAsync();Network Engine
By default, SimpleWServer uses SimpleWEngine as its network engine.
Configure the default socket engine with UseEngine(Action<SimpleWEngineOptions>) before starting the server.
var server = new SimpleWServer(IPAddress.Any, 8080);
server.UseEngine(options => {
options.TcpNoDelay = true;
options.ReuseAddress = true;
options.TcpKeepAlive = true;
});Unix Sockets
Unix Domain Socket (UDS) can also be used as an entrypoint for the server. They are supported on : Linux, MacOS, Android... and even Windows !
With just a small change, the basic api example can also be served over a Unix socket.
string unixSocketPath = @"C:\www\server.sock";
var server = new SimpleWServer(new UnixDomainSocketEndPoint(unixSocketPath));You can use curl to test :
$ curl --unix-socket C:\www\test.sock http://localhost/api/test
> { "message" : "Hello World !" }There is only one change :
- L13 : use the
SimpleWServer()constructor withUnixDomainSocketEndPointargument.
Starting the server
StartAsync (non-blocking)
StartAsync() starts listening and returns immediately.
await server.StartAsync();
// app continues running...
Console.WriteLine("Server started!");This is useful if you already have your own host loop (service, worker, etc.) and you just want to start/stop the server as part of a bigger lifecycle.
RunAsync (blocking)
RunAsync() is a convenience method: it starts the server and then waits until the server is stopped or the provided token is cancelled.
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
await server.RunAsync(cts.Token);Stopping the server
StopAsync() triggers shutdown. Internally it :
- cancels the lifetime token
- closes the listen socket
- closes all sessions
- disposes timers and telemetry
- resets server state
await server.StopAsync();Calling StopAsync() multiple times is safe. Concurrent calls are serialized and the engine is stopped only once. If an engine stop fails, local cleanup still completes, State becomes Faulted, and a later StopAsync() retries engine cleanup.
Reloading the listener at runtime
ReloadListenerAsync(...) lets you change listening settings while the server is running.
- The server must already be started, otherwise it throws.
- It temporarily stops accepting new connections (closes the listen socket), applies your
reconfigure(server)callback, then starts listening again. - Existing sessions are not the listener: reloading focuses on the accept loop + endpoint. Engine-specific TLS settings can also be changed if the selected engine exposes that API.
await server.ReloadListenerAsync(s => {
s.UseAddress(IPAddress.Any);
s.UsePort(9090);
// Optional for the default socket engine:
// if (s.Engine is SimpleWEngine engine) engine.UseHttps(newSslContext);
});If the reload fails, the server attempts a best-effort rollback to the previous endpoint and throws a ListenerReloadException.
try {
await server.ReloadListenerAsync(s => s.UsePort(9090));
}
catch (ListenerReloadException ex) when (ex.ListenerRestored) {
// The requested configuration failed, but the previous listener is active again.
Debug.Assert(server.State == SimpleWServerState.Started);
Console.Error.WriteLine(ex.ReloadException);
}
catch (ListenerReloadException ex) {
// Both the reload and the rollback failed. The listener is unavailable.
Debug.Assert(server.State == SimpleWServerState.Faulted);
Console.Error.WriteLine(ex.ReloadException);
Console.Error.WriteLine(ex.RollbackException);
}ReloadExceptionis the original error raised while applying or starting the new listener.RollbackExceptionisnullwhen the previous listener was restored; otherwise it contains the restoration error.ListenerRestoredistrueonly when the rollback completed successfully.
When ListenerRestored is true, State is Started. When both reload and rollback fail, State is Faulted; use StopAsync() to attempt recovery before starting again.
Restoring the listener keeps the server available, but the requested reload still failed and is therefore always reported to the caller.
Procedural vs Fluent usage
Procedural style
var server = new SimpleWServer(IPAddress.Any, 8080);
server.Configure(options => { /* ... */ });
server.MapGet("/", (HttpSession ctx) => "Hello");
await server.RunAsync();Fluent style
Most configuration methods return SimpleWServer, so you can chain them :
await new SimpleWServer(IPAddress.Any, 8080)
.Configure(options => { /* ... */ })
.MapGet("/", ctx => "Hello")
.RunAsync();