Server-to-Client Calls (Reverse RPC)
ActualLab.Rpc supports bidirectional communication, allowing the server to call methods on clients. This is useful for push notifications, real-time updates, or any bidirectional communication pattern.
How It Works
In a typical RPC setup, clients call server methods. Reverse RPC flips this: the server can invoke methods on a connected client. The flow is:
- Client connects to server and registers a client-side service
- Server receives a call from the client
- Server uses
RpcInboundContextto get the calling peer (client) - Server uses
RpcOutboundCallSetupto route a call back to that specific client
Defining Client-Side Services
Define an interface for the service that will run on the client:
public interface ISimpleClientSideService : IRpcService
{
// Server calls this on the client
Task<RpcNoWait> Pong(string message);
}TIP
Using Task<RpcNoWait> is recommended for server-to-client calls since the server typically doesn't need to wait for the client's response.
Implementing the Client-Side Service
The client implements the service and registers it with DI:
public class SimpleClientSideService : ISimpleClientSideService
{
public Channel<string> PongChannel { get; } = Channel.CreateUnbounded<string>();
public Task<RpcNoWait> Pong(string message)
{
// Process the incoming message from the server
_ = PongChannel.Writer.WriteAsync(message);
return RpcNoWait.Tasks.Completed;
}
}Register it in the client's DI container:
services.AddSingleton<SimpleClientSideService>();
services.AddSingleton<ISimpleClientSideService>(c => c.GetRequiredService<SimpleClientSideService>());Calling the Client from the Server
The server uses RpcInboundContext to identify the calling client and RpcOutboundCallSetup to route the response:
public class SimpleService(ISimpleClientSideService clientSideService) : ISimpleService
{
public async Task<RpcNoWait> Ping(string message)
{
// Get the peer (client) that made this call
var peer = RpcInboundContext.GetCurrent().Peer;
// Route the call to that specific peer
Task<RpcNoWait> pongTask;
using (new RpcOutboundCallSetup(peer).Activate()) // No "await" inside this block!
pongTask = clientSideService.Pong($"Pong to '{message}'");
await pongTask.ConfigureAwait(false);
return default;
}
}RpcOutboundCallSetup
RpcOutboundCallSetup controls how outbound RPC calls are routed. It's essential for reverse RPC because it lets you specify exactly which peer should receive the call.
Basic Usage
// Route call to a specific peer
using (new RpcOutboundCallSetup(peer).Activate())
task = service.Method();
await task;Critical
Never use await inside the using (....Activate()) block. Capture the task and await it after the block ends. The setup is consumed when the call starts, so awaiting inside would cause issues with subsequent calls.
Constructors
| Constructor | Description |
|---|---|
RpcOutboundCallSetup() | Creates setup with RpcRoutingMode.Outbound (default routing) |
RpcOutboundCallSetup(RpcPeer peer) | Routes to specific peer with RpcRoutingMode.Prerouted |
RpcOutboundCallSetup(RpcPeer peer, RpcRoutingMode mode) | Routes to specific peer with custom routing mode |
Routing Modes
RpcRoutingMode controls how the call is routed:
| Mode | Description |
|---|---|
Outbound | Default routing via RpcMethodDef.RouteOutboundCall |
Inbound | Routes via RpcMethodDef.RouteInboundCall |
Prerouted | Call is pre-routed to the specified peer (used with reverse RPC) |
Optional Properties
var setup = new RpcOutboundCallSetup(peer) {
Headers = [...], // Custom RPC headers (rarely needed)
CacheInfoCapture = ..., // For cache info capture scenarios
};Accessing the Produced Context
After the call starts, you can access the RpcOutboundContext that was created:
var setup = new RpcOutboundCallSetup(peer);
Task task;
using (setup.Activate())
task = service.Method();
var context = setup.ProducedContext; // Available after call starts
await task;Complete Example
See the TodoApp RpcExamplePage for a complete working example demonstrating bidirectional ping-pong communication.
The example shows:
- Client sending ping messages to the server
- Server responding with pong messages back to the client
- Real-time UI updates as messages are exchanged
