Getting started
JSON-RPC is a peer-to-peer protocol. Either party may expose methods, invoke methods exposed by the other party, or do both over the same connection.
Install the package
Consume Nerdbank.JsonRpc via its NuGet package. The badge links to the latest version and installation instructions.
Define an RPC contract
Define an interface shared by the local target and the strongly typed proxy. GenerateShapeAttribute generates the method metadata used to dispatch incoming calls, while GenerateJsonRpcProxyAttribute generates the proxy used to make outgoing calls.
[GenerateJsonRpcProxy]
[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]
public partial interface ICalculator
{
ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken);
}
Create a JSON-RPC connection
Nerdbank.JsonRpc exchanges messages through an IDuplexPipe. Most applications begin with a bidirectional Stream, which may come from a TCP connection, named pipe, Unix-domain socket, child process standard input/output, or another application-specific transport. Use UsePipe to adapt the stream, then create a channel and the JsonRpc connection:
public static JsonRpc CreateConnection(Stream stream, ILogger logger)
{
IDuplexPipe pipe = stream.UsePipe();
var channel = new JsonRpcMessagePackChannel(pipe);
return new JsonRpc(channel) { Logger = logger };
}
The channel defines the wire encoding and framing. This example uses MessagePack with its default length-header framing. Both parties must use compatible encoding and framing; see Encodings and framing for JSON and other choices.
The application is responsible for establishing the connected stream and managing its lifetime. The RPC connection automatically tears itself down on EOF or failure. Deliberate disposal and EOF without pending calls complete its lifetime task successfully; EOF with pending calls and other failures fault it. Inspect the preserved original cause even after disposal, and do not reuse a terminated connection. See Connection lifecycle for the complete completion, exception, state, and peer-diagnostic contract. For in-process tests, CreatePipePair creates two connected IDuplexPipe instances that can be given directly to two channels.
For same-machine IPC between mutually trusted processes running as the same user, consider SharedMemoryDuplexPipe. This example connects both endpoints in one process for simplicity; separate processes can use the same shared name:
/// <summary>Calls the calculator over a shared-memory connection whose endpoints are both in this process.</summary>
/// <param name="cancellationToken">Cancels endpoint setup or the RPC request.</param>
/// <returns>The sum returned by the calculator.</returns>
public static async Task<int> CallOverSharedMemoryAsync(CancellationToken cancellationToken)
{
string name = Guid.NewGuid().ToString("N");
(SharedMemoryDuplexPipe clientPipe, SharedMemoryDuplexPipe serverPipe) = await ConnectSharedMemoryEndpointsAsync(name, cancellationToken);
using (clientPipe)
using (serverPipe)
{
using JsonRpc server = new(new JsonRpcMessagePackChannel(serverPipe));
server.AddRpcTarget<ICalculator>(new Calculator());
server.Start();
using JsonRpc client = new(new JsonRpcMessagePackChannel(clientPipe));
client.Start();
return await client.Attach<ICalculator>().AddAsync(1, 2, cancellationToken);
}
}
The transport is supported on .NET 8+ (Windows and Unix) and .NET Framework 4.7.2 (Windows); the netstandard2.0 asset throws PlatformNotSupportedException. Since either peer can modify shared memory, this is not a security boundary for untrusted peers or different privilege levels. See the Nerdbank.Streams security guidance, and benchmark against named pipes for your workload.
Expose local methods to the remote party
Implement the contract as an ordinary .NET type:
public sealed class Calculator : ICalculator
{
public ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken) => new(a + b);
}
Register the target before starting the connection. Start begins reading and dispatching messages; awaiting Completion keeps this endpoint active until the connection closes or faults.
using JsonRpc rpc = CreateConnection(stream, logger);
rpc.AddRpcTarget<ICalculator>(new Calculator());
rpc.Start();
await rpc.Completion;
A connection may register any number of targets. Registering a target does not make this endpoint exclusively a "server"; the same connection can also invoke methods on the remote party.
Call methods exposed by the remote party
Start the connection, attach the generated proxy, and invoke it like an ordinary interface. The proxy sends the request to the remote party that registered the matching target.
using JsonRpc rpc = CreateConnection(stream, logger);
rpc.Start();
ICalculator calculator = rpc.Attach<ICalculator>();
int sum = await calculator.AddAsync(1, 2, cancellationToken);
The same JsonRpc instance may both register local targets and attach remote proxies, enabling calls in either direction.
Connect to another JSON-RPC implementation
The remote party does not need to use Nerdbank.JsonRpc. The two parties must agree on the wire protocol details:
- Select compatible serialization and message boundaries as described in Encodings and framing.
- Match the remote method names using method name transforms or explicit PolyType method names.
- Match its positional or named parameter convention using generated client proxy options.
- When applicable, use the documented RPC-marshalable and exotic-type protocols, which are compatible with StreamJsonRpc.
Where to go from here
- Learn which interface members and method signatures can be used by generated client proxies.
- Understand request cancellation and
CancellationTokenpropagation in Protocol behavior. - Pass objects by reference, observers, progress callbacks, streams, and asynchronous sequences using RPC-marshalable interfaces and exotic types.
- Send several requests in one payload with Batching.
- Propagate .NET events as JSON-RPC notifications with Events as notifications.