Both sides of a WitRPC connection are configured through fluent builders: WitServerBuilder on the server, WitClientBuilder on the client. This page covers the full setup cycle: building, starting, connecting, handling errors, and shutting down.
Building a server
A server needs at minimum a service, a transport, and a serializer:
var server = WitServerBuilder.Build(options =>
{
options.WithService(new ExampleService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithJson();
});The main option groups:
- Service registration.
WithService(instance)registers one implementation. To host several interfaces on one server, useWithServices().AddService<I>(impl)...Build(); see Composite Services →. - Transport. One of
WithTcp,WithWebSocket,WithNamedPipe,WithMemoryMappedFile, orWithRest. Choosing between them is covered in Transports & Serialization →. - Serializer.
WithJson()by default;WithMessagePack(),WithMemoryPack(), orWithProtoBuf()for binary formats. - Security.
WithEncryption()andWithAccessToken(...); details in Security & Authentication →. - Diagnostics.
WithLogger(...)plugs in logging;WithTimeout(...)sets the communication timeout.
Starting and stopping
Building produces a WitServer that is not yet listening. Start and stop it explicitly:
server.StartWaitingForConnection();
Console.WriteLine("Server started. Press any key to stop...");
Console.ReadKey();
server.StopWaitingForConnection();
Console.WriteLine("Server stopped.");StartWaitingForConnection() launches the listening loop on background threads and returns immediately, so the application continues running. StopWaitingForConnection() shuts down gracefully: it closes the listener, disconnects clients, and releases resources.
Server configurations by environment
Development favors debuggability; production favors performance and security; local IPC sits in between.
// Development: human-readable traffic, no security friction
var devServer = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 10);
options.WithJson();
});
// Production: binary serializer, encryption, authentication
var prodServer = WitServerBuilder.Build(options =>
{
options.WithServices()
.AddService<IUserService>(new UserService())
.AddService<IOrderService>(new OrderService())
.Build();
options.WithTcp(5000, maxNumberOfClients: 1000);
options.WithMessagePack();
options.WithEncryption();
options.WithAccessToken(secretToken);
});
// Local IPC: named pipe stays on the machine
var ipcServer = WitServerBuilder.Build(options =>
{
options.WithService(new ProcessingService());
options.WithNamedPipe("MyApp_IPC");
options.WithMessagePack();
});For hosting inside ASP.NET Core with automatic startup and DI-resolved services, see Dependency Injection →.
Building a client
The client mirrors the server's transport, serializer, and security settings:
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("localhost", 5000);
options.WithJson();
});Mirroring matters: a serializer mismatch, encryption enabled on only one side, or a wrong token each cause the handshake to fail. Keeping the shared parts of the configuration in one place (constants or configuration files used by both projects) prevents drift.
Connecting
ConnectAsync takes a timeout and a cancellation token, and returns whether the connection succeeded:
bool connected = await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
if (!connected)
{
Console.WriteLine("Failed to connect to the server.");
return;
}During the call, WitRPC opens the transport, runs the encryption handshake if enabled, validates the access token if the server requires one, and negotiates the protocol. A false result means one of those steps failed. The usual causes:
- the server is not running, or the address, port, or pipe name is wrong;
- the transport types differ between client and server;
- encryption is enabled on one side only, or the encryption modes differ;
- the access token is missing or invalid;
- the network did not respond within the timeout.
WithLogger(...) on both sides is the fastest way to see which step failed.
Timeout values depend on the environment: a few seconds is enough locally or on a LAN; internet connections may need 10 to 30 seconds; mobile networks warrant longer timeouts plus retry logic.
Connection retry
A simple retry loop covers transient startup races, such as a client launching before its server:
public async Task<WitClient?> ConnectWithRetryAsync(int maxAttempts = 3)
{
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("localhost", 5000);
options.WithJson();
});
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
if (await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None))
return client;
if (attempt < maxAttempts)
await Task.Delay(TimeSpan.FromSeconds(2));
}
return null;
}For ongoing resilience after the initial connection (dropped links, server restarts), the built-in auto-reconnect is the right tool: options.WithAutoReconnect(...), covered in Resilience & Health Checks →.
Watching for disconnection
The client raises Disconnected when the connection is lost. The handler receives the connection id:
client.Disconnected += sender =>
{
Console.WriteLine("Disconnected from server.");
// Notify the user, trigger reconnection logic, etc.
};Getting the proxy and making calls
Once connected, request a proxy for the contract:
var service = client.GetService<IExampleService>();
service.ProgressChanged += p => Console.WriteLine($"Progress: {p}%");
var result = await service.ProcessDataAsync("input");The proxy implements the interface locally and forwards calls to the server; server events arrive at subscribed handlers. With composite services, call GetService<T>() once per interface over the same connection.
Two practical notes. Subscribe to events after obtaining the proxy, so subscriptions register on the live connection. And expect event handlers to run on background threads: UI applications must marshal updates to the UI thread (a dispatcher in WPF or WinUI, InvokeAsync(StateHasChanged) in Blazor).
Disconnecting
Close the connection when done:
await client.DisconnectAsync();This lets the server release resources tied to the client. In applications with dependency injection or IDisposable patterns, dispose the client as part of normal cleanup.
Next
- Transports & Serialization → for choosing the channel and format.
- Security & Authentication → for encryption and tokens.
- Resilience & Health Checks → for auto-reconnect, retry policies, and monitoring.