Minimal C# source generator that produces a JSON-RPC-like client/server stub layer from two interfaces and a single attribute.
The repository includes a runnable demo in RpcGen.Sample (targets .NET 10 in RpcGen.Sample.csproj).
When you annotate a base class with [RpcInterface(typeof(TOutbound), typeof(TInbound))], the generator emits:
- A small runtime file:
RpcRuntime.g.cs
IRpcTransport(send bytes + async stream of received messages)WebSocketTransport(adapter overSystem.Net.WebSockets.WebSocket)RpcCore(framing, request/response correlation, dispatch)RpcJson.DefaultOptions(System.Text.Jsonoptions)RpcRemoteException(raised when a response containserr)
- A per-base-class stub file:
*.Rpc.g.cs
Generated members on the base class:
AttachTransport(IRpcTransport transport, JsonSerializerOptions? jsonOptions = null)(protected)RunAsync(CancellationToken cancellationToken = default)(public)- inbound dispatcher:
OnRequestAsync(...) - outbound stub method implementations for
TOutbound - request/response DTOs per method
- abstract inbound handlers for
TInbound(you implement these in your derived concrete type)
RpcGen.SourceGenerator- Contains
RpcGeneratorand the embedded runtime templateRuntime/RpcRuntime.cs.
- Contains
RpcGen.SourceGenerator.Test- Roslyn tests that validate generated output compiles.
RpcGen.Sample- End-to-end demo.
RpcGen.Sample references the generator as an analyzer (see RpcGen.Sample/RpcGen.Sample.csproj):
<ItemGroup>
<ProjectReference Include="..\RpcGen.SourceGenerator\RpcGen.SourceGenerator\RpcGen.SourceGenerator.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>You also need a normal reference to RpcGen.Abstractions for [RpcInterface]:
<ItemGroup>
<ProjectReference Include="..\RpcGen.Abstractions\RpcGen.Abstractions.csproj" />
</ItemGroup>RpcGen expects two interfaces to model duplex communication:
TOutbound: methods you call on the remote endpointTInbound: methods the remote endpoint can call on you
Example names from the demo:
IClientRpcInterface: client ? serverIServerRpcInterface: server ? client
Supported method return types (both inbound and outbound):
void(notification)TaskTask<T>
You create one base class per endpoint role and annotate each with opposite directions.
using RpcGen;
[RpcInterface(typeof(IServerRpcInterface), typeof(IClientRpcInterface))]
internal abstract partial class ServerAppRpcBaseClass : IServerRpcInterface
{
}
[RpcInterface(typeof(IClientRpcInterface), typeof(IServerRpcInterface))]
internal abstract partial class ClientAppRpcBaseClass : IClientRpcInterface
{
}The generator emits abstract methods for TInbound. Your concrete types implement them:
ServerApp : ServerAppRpcBaseClassimplements handlers forIClientRpcInterface.ClientApp : ClientAppRpcBaseClassimplements handlers forIServerRpcInterface.
Each base class gets:
AttachTransport(...)to wire anIRpcTransportRunAsync(...)to start reading and dispatching inbound messages
Minimal wiring pattern:
// server.AttachTransport(serverTransport);
// client.AttachTransport(clientTransport);
var serverLoop = server.RunAsync(ct);
var clientLoop = client.RunAsync(ct);Inbound dispatch is driven by the JSON fields produced/consumed by RpcCore:
id: request idi: interface namem: method namek: kind (Request,Response,Notification)p: payload objecterr: error string (responses only)
IRpcTransport is intentionally small:
SendAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)ReadAsync(CancellationToken cancellationToken)yieldingIAsyncEnumerable<ReadOnlyMemory<byte>>
RpcCore assumes each yielded chunk from ReadAsync is a complete JSON message.
The runtime includes WebSocketTransport as an adapter over a WebSocket which reads full text messages and yields them as UTF-8 bytes.
Outbound interface methods are implemented on the generated base class:
void? notification (RpcCore.NotifyAsync)Task/Task<T>? request/response (RpcCore.RequestAsync)
Inbound calls are dispatched by OnRequestAsync(...) to the abstract handler methods you implement.
voidinbound handlers do not send a response.Task/Task<T>inbound handlers send a response payload back to the caller.
RpcGen.Sample is a minimal end-to-end demo showing:
- one server and one client (separate concrete types)
- an in-memory paired transport
- client ? server requests and server ? client notifications
Entry point: RpcGen.Sample/Program.cs.
- Serialization uses
System.Text.Json. - Generated DTO property names are derived from parameter names (first letter uppercased).
- Only ordinary interface methods are considered (no properties/events).
- Return types beyond
void,Task, andTask<T>are treated as unsupported.