-
Notifications
You must be signed in to change notification settings - Fork 16
RPC Server: Getting Started
This page will provide guidance on how to implement your own JSON-RPC server using JsonRpc.Standard.
The following diagram describes how JsonRpc.Standard server-side framework handles the incoming JSON-RPC Request
s.
Basically, an implementation of JsonRpcServerHandler
will monitor for incoming requests, and when a request has reached, it transfers it to IJsonRpcServiceHost
, which will dispatch the request, and return the result. Finally, JsonRpcServerHandler
will transmit the response, if needed.
Though there're a lot of interfaces on the diagram, most of them has corresponding default implementations, and you only have to implement IJsonRpcService class.
It's somewhat like the implementation of a Controller
in ASP.NET. You need to derive a class from JsonRpcService
, write your methods, and mark them either as a request or a notification with JsonRpcMethodAttribute
.
public class LibraryService : JsonRpcService
{
// The service instance is transcient. You cannot persist state in such a class.
// So we need session.
private LibrarySessionFeature Session => RequestContext.Features.Get<LibrarySessionFeature>();
[JsonRpcMethod]
public Book GetBook(string isbn, bool required = false)
{
var book = Session.Books.FirstOrDefault(b => AreIsxnEqual(b.Isbn, isbn));
if (required && book == null)
throw new JsonRpcException(new ResponseError(1000, $"Cannot find book with ISBN:{isbn}."));
return book;
}
[JsonRpcMethod]
public ResponseError PutBook(Book book)
{
// Yes, you can just throw an ordinary Exception… Though it's not recommended.
if (book == null) throw new ArgumentNullException(nameof(book));
if (string.IsNullOrEmpty(book.Isbn))
return new ResponseError(1001, $"Missing Isbn field of the book: {book}.");
var index = Session.Books.FindIndex(b => AreIsxnEqual(b.Isbn, book.Isbn));
if (index > 0)
Session.Books[index] = book;
else
Session.Books.Add(book);
return null;
}
[JsonRpcMethod]
public IEnumerable<string> EnumBooksIsbn()
{
return Session.Books.Select(b => b.Isbn);
}
[JsonRpcMethod(IsNotification = true)]
public void Terminate()
{
Session.StopServer();
}
private bool AreIsxnEqual(string x, string y)
{
// ...
}
}
Some points worth noting:
- The default JSON RPC method name is the CLR method name. You can specify
CamelCaseJsonRpcNamingStrategy
inJsonRpcMethodAttribute
or inJsonRpcContractResolver.NamingStrategy
- You can either implement your method synchronously or asynchronously, i.e. returning a
Task
orTask<T>
is possible. -
CancellationToken
in the parameters is a synonym ofRequestContext.CancellationToken
.- For a simple server-side implementation of operation cancellation, please take a look at
TestJsonRpcService.CancelRequest
inUnitTestProject1
. As for$/cancelRequest
implementation in Language Server, seeInitializaionService.CancelRequest
in CXuesong/LanguageServer.NET.
- For a simple server-side implementation of operation cancellation, please take a look at
- Optional parameters are supported. If some arguments are missing in the JSON-RPC request, their default values will be used.
- You may specify
AllowExtensionData = true
in the attribute to allow extra parameters passed to the method. You can later extract the parameters fromRequestContext.Request
property. - Method-overloading is supported by distingushing the method parameter count and type (Number/String/Array/Object).
- You can report an error by returning a
ResponseError
object or throwing aJsonRpcException
in your service implementation. - You can set the response by manually setting
RequestContext.Response
property. This will override the return value of the JSON-RPC method handler. - You can control the granularity of the settings by using, e.g.,
JsonRpcScopeAttribute
&JsonRpcParameterAttribute
.- Especially, you may add a common prefix for all the JSON-RPC method names in a service class by applying
[JsonRpcScope(MethodPrefix = "prefix.")]
.
- Especially, you may add a common prefix for all the JSON-RPC method names in a service class by applying
- By default, service classes must have a public parameterless constructor. Or you need to implement your own
IServiceFactory
and pass it toServiceHostBuilder
.- If you need to use DI, consider passing your service provider as some
Feature
, and in your ownIServiceFactory
implementation, you may get the service provider fromRequestContext.Features
; then you may create the service instances using DI. SeeJsonRpc.AspNetCore.HttpContextServiceFactory
for an example implementation.
- If you need to use DI, consider passing your service provider as some
First of all, we need to put all the service classes together, and do some basic configurations, so that it can actually process the RequestMessage
s, and return the expected ResponseMessage
s. This is where IJsonRpcServiceHost
comes in. To configure the services, you need JsonRpcServiceHostBuilder
, which can build IJsonRpcServiceHost
instances. you may write something like this
private static IJsonRpcServiceHost BuildServiceHost()
{
var builder = new JsonRpcServiceHostBuilder
{
ContractResolver = new JsonRpcContractResolver
{
// Use camelcase for RPC method names.
NamingStrategy = new CamelCaseJsonRpcNamingStrategy(),
// Use camelcase for the property names in parameter value objects
ParameterValueConverter = new CamelCaseJsonValueConverter()
},
};
// Register all the services (public classes) found in the assembly
builder.Register(typeof(Program).GetTypeInfo().Assembly);
// Add a middleware to log the requests and responses
builder.Intercept(async (context, next) =>
{
Console.WriteLine("> {0}", context.Request);
await next();
Console.WriteLine("< {0}", context.Response);
});
return builder.Build();
}
We will use IJsonRpcServiceHost
to process the incoming requests. Note that IJsonRpcServiceHost.InvokeAsync
not only is asynchronous, but also supports concurrent calls.
Hooray now we're just ready to process the requests. So, the final problem: where is the channel for the service host to receive and transmit the JSON-RPC messages?
Currently, JsonRpc.Standard supports the
- JSON-RPC calls over
Stream
s/TextReader/TextWriter
s - JSON-RPC calls over HTTP
For testing purposes, here we use ByLineTextMessageReader
and ByLineTextMessageWriter
to read and write JSON-RPC messages line-by-line from and to the console.
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
// Configure & build service host
var host = BuildServiceHost();
// Use MessageReader/MessageWriter to read and write messages.
var serverHandler = new StreamRpcServerHandler(host);
// Though it's suggested that all feature types be interface types, for sake of
// simplicity, here we just use a concrete class.
var session = new LibrarySessionFeature();
serverHandler.DefaultFeatures.Set(session);
// Messages come from Console
using (var reader = new ByLineTextMessageReader(Console.In))
using (var writer = new ByLineTextMessageWriter(Console.Out))
using (serverHandler.Attach(reader, writer))
{
// Wait for exit
session.CancellationToken.WaitHandle.WaitOne();
}
Console.WriteLine("Server exited.");
}
The following lines may indicate the runtime situation. Lines beginning with ">" are user's input; lines beginning with "<" are JSON-RPC server's output.
> {"id":"63835064#1","method":"putBook","params":{"book":{"title":"Somewhere Within the Shadows","author":"Juan Díaz Canales & Juanjo Guarnido","publishDate":"2004-01
-01T00:00:00","isbn":"1596878177"}},"jsonrpc":"2.0"}
< {"id":"63835064#1","result":null,"jsonrpc":"2.0"}
> {"id":"63835064#2","method":"putBook","params":{"book":{"title":"Arctic Nation","author":"Juan Díaz Canales & Juanjo Guarnido","publishDate":"2004-01-01T00:00:00","
isbn":"0743479351"}},"jsonrpc":"2.0"}
< {"id":"63835064#2","result":null,"jsonrpc":"2.0"}
Available books:
> {"id":"63835064#3","method":"enumBooksIsbn","jsonrpc":"2.0"}
< {"id":"63835064#3","result":["1596878177","0743479351"],"jsonrpc":"2.0"}