In my https://github.com/AArnott/Nerdbank.JsonRpc/ repo I have a much simpler JSON-RPC library than StreamJsonRpc makes possible, thanks to PolyType. The server-side is fully functional and quite nice. (It assumes Nerdbank.MessagePack as the serializer, which simplifies some things significantly, again thanks in part to PolyType).
The client side of the library though leaves more for the user to manually code than I'd like, however.
- This library currently requires all the arguments to be serialized as a map or array via a single declared type for which we have a type shape.
- The convenient calling pattern of invoking the RPC interface requires a proxy to be manually written that uses the argument type.
Here is a sample of the code the user must manually write for a one-method RPC interface:
internal partial class ServerProxy(JsonRpc jsonRpc) : IServer
{
public ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken)
=> jsonRpc.RequestAsync<AddArgs, int, Witness>(nameof(IServer.AddAsync), new AddArgs { a = a, b = b }, cancellationToken);
[GenerateShape]
internal partial struct AddArgs
{
[Key(0)]
public int a;
[Key(1)]
public int b;
}
}
I started exploring a source generator of my own that would activate when the user code has something like this:
[RpcProxy]
internal partial class MyProxy : IServer { }
The source generator would then emit a partial declaration that fills in the above code. The problem is the above code includes PolyType attributes, which as a source generator itself, means PolyType won't actually generate type shapes for this source generated code.
I'd like to explore whether PolyType could optionally generate a proxy for me, based on the user's partial declaration (so they can control which interfaces are implemented). It might look like this:
The user declares a partial proxy class like this:
[GenerateProxy]
internal partial class MyProxy : IServer { }
PolyType's source generator then emits another partial, like this:
internal partial class MyProxy : IServer
{
private readonly IMethodInvoker invoker;
internal MyProxy(IMethodInvoker invoker) => this.invoker = invoker;
ValueTask<int> IServer.AddAsync(int a, int b, CancellationToken cancellationToken)
=> this.invoker.Invoke<ValueTuple<int, int, CancellationToken>, int>("AddAsync", (a, b, cancellationToken), GeneratedTypeShapeProvider.ABCTShape);
}
The PolyType library itself would declare this type:
public interface IMethodInvoker
{
T Invoke<TArg, TReturn>(string method, TArg arg, ITypeShape<TArg> shape);
}
My library would then be responsible to implement IMethodInvoker, and its implementation would have used the type shape visitor pattern on the IServer interface (in advance) in order to construct that IMethodInvoker instance so that it knows how to handle all the methods.
There's an open question about how PolyType's source generator knows which of the interfaces are meant to be implemented by PolyType.
The way in which Nerdbank.JsonRpc would implement IMethodInvoker using the visitor pattern is a bit hand wavey at this point, but I suspect it could work.
Thoughts?
In my https://github.com/AArnott/Nerdbank.JsonRpc/ repo I have a much simpler JSON-RPC library than StreamJsonRpc makes possible, thanks to PolyType. The server-side is fully functional and quite nice. (It assumes Nerdbank.MessagePack as the serializer, which simplifies some things significantly, again thanks in part to PolyType).
The client side of the library though leaves more for the user to manually code than I'd like, however.
Here is a sample of the code the user must manually write for a one-method RPC interface:
I started exploring a source generator of my own that would activate when the user code has something like this:
The source generator would then emit a partial declaration that fills in the above code. The problem is the above code includes PolyType attributes, which as a source generator itself, means PolyType won't actually generate type shapes for this source generated code.
I'd like to explore whether PolyType could optionally generate a proxy for me, based on the user's partial declaration (so they can control which interfaces are implemented). It might look like this:
The user declares a partial proxy class like this:
PolyType's source generator then emits another partial, like this:
The PolyType library itself would declare this type:
My library would then be responsible to implement
IMethodInvoker, and its implementation would have used the type shape visitor pattern on theIServerinterface (in advance) in order to construct thatIMethodInvokerinstance so that it knows how to handle all the methods.There's an open question about how PolyType's source generator knows which of the interfaces are meant to be implemented by PolyType.
The way in which Nerdbank.JsonRpc would implement
IMethodInvokerusing the visitor pattern is a bit hand wavey at this point, but I suspect it could work.Thoughts?