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:

csharp
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, use WithServices().AddService<I>(impl)...Build(); see Composite Services →.
  • Transport. One of WithTcp, WithWebSocket, WithNamedPipe, WithMemoryMappedFile, or WithRest. Choosing between them is covered in Transports & Serialization →.
  • Serializer. WithJson() by default; WithMessagePack(), WithMemoryPack(), or WithProtoBuf() for binary formats.
  • Security. WithEncryption() and WithAccessToken(...); 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:

csharp
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.

csharp
// 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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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