This walkthrough builds a minimal working example: a service with methods and events, a server hosting it over WebSocket, and a client that calls a method and receives an event callback. By the end you will have seen the full WitRPC loop, from interface to remote call to real-time notification.
Installation
Add the WebSocket transport packages to the two projects. Each transport package brings in the WitRPC core with it.
# Server project:
dotnet add package OutWit.Communication.Server.WebSocket
# Client project:
dotnet add package OutWit.Communication.Client.WebSocketStep 1: Define a service interface
The service contract is a shared C# interface. It declares the methods clients can call and the events they can subscribe to:
public interface IExampleService
{
// Events the server raises to notify clients:
event Action ProcessingStarted;
event Action<double> ProgressChanged;
event Action<string> ProcessingCompleted;
// Methods clients call remotely:
bool StartProcessing();
void StopProcessing();
Task<string> ProcessDataAsync(string data);
}Both sides compile against this interface. The server implements it; the client receives a proxy for it.
Step 2: Implement the service
The implementation lives on the server and contains the actual logic. Raising an event here delivers it to every subscribed client:
public class ExampleService : IExampleService
{
// Initialize events with empty delegates to avoid null checks
public event Action ProcessingStarted = delegate { };
public event Action<double> ProgressChanged = delegate { };
public event Action<string> ProcessingCompleted = delegate { };
public bool StartProcessing()
{
ProcessingStarted(); // Notify subscribers
return true;
}
public void StopProcessing()
{
// Stop logic omitted in this minimal example
}
public async Task<string> ProcessDataAsync(string data)
{
await Task.Delay(100); // Simulate work
return $"Processed: {data}";
}
}In a fuller implementation, ProcessDataAsync would report progress through ProgressChanged(percent) along the way and finish with ProcessingCompleted(result); the events are declared here so the client side can show the subscription pattern.
Step 3: Set up the server
The server is configured through a fluent builder. This example listens on WebSocket port 5000, serializes with JSON, enables encryption, and requires an access token:
var server = WitServerBuilder.Build(options =>
{
options.WithService(new ExampleService());
options.WithWebSocket("http://localhost:5000", maxClients: 100);
options.WithJson();
options.WithEncryption();
options.WithAccessToken("your-secret-token");
});
server.StartWaitingForConnection();WithService registers the implementation clients will reach. After the builder runs, StartWaitingForConnection() begins listening for clients.
Step 4: Connect the client
The client mirrors the server's configuration: same transport, same serializer, same security settings. Note the ws:// scheme on the client side:
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket("ws://localhost:5000");
options.WithJson();
options.WithEncryption();
options.WithAccessToken("your-secret-token");
});
await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
var service = client.GetService<IExampleService>();ConnectAsync establishes the connection, waiting up to five seconds. GetService<IExampleService>() then returns the proxy: an object that implements the interface locally and forwards every call to the server.
Security settings must match on both ends. A client without the right token, or with encryption settings different from the server's, is rejected during the handshake.
Step 5: Call a method and receive an event
With the proxy in hand, remote calls look like local ones, and server events arrive at ordinary C# event handlers:
// Subscribe before calling methods, so no callback is missed
service.ProcessingStarted += () => Console.WriteLine("Processing Started");
service.ProgressChanged += progress => Console.WriteLine($"Progress: {progress}%");
service.ProcessingCompleted += result => Console.WriteLine($"Completed: {result}");
// Invoke remote methods
service.StartProcessing();
var result = await service.ProcessDataAsync("Hello");
Console.WriteLine($"Result from server: {result}");Calling service.StartProcessing() executes ExampleService.StartProcessing() on the server, which raises ProcessingStarted; the client's handler prints its message in real time. ProcessDataAsync("Hello") runs on the server and returns its result to the awaiting client. The output:
Processing Started
Result from server: Processed: HelloWhat you built
A shared interface, a server hosting its implementation, and a client calling it through a proxy, with a server-raised event delivered back in real time. That is the whole WitRPC pattern; everything else in the framework builds on it.
From here:
- Service Contracts → covers designing interfaces well: types, async methods, event signatures.
- Transports & Serialization → helps pick the right channel and format for your scenario.
- Security & Authentication → goes deeper on encryption and tokens, including Blazor WebAssembly.