Skip to content

RPC Server: Getting Started

Chen edited this page May 30, 2017 · 9 revisions

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 Requests.

server-uml-seq

Basically, an implementation of JsonRpcServerHandlerwill 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.

Write a service 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 in JsonRpcMethodAttribute or in JsonRpcContractResolver.NamingStrategy
  • You can either implement your method synchronously or asynchronously, i.e. returning a Task or Task<T> is possible.
  • CancellationToken in the parameters is a synonym of RequestContext.CancellationToken.
  • 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 from RequestContext.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 a JsonRpcException 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.")].
  • By default, service classes must have a public parameterless constructor. Or you need to implement your own IServiceFactory and pass it to ServiceHostBuilder.
    • If you need to use DI, consider passing your service provider as some Feature, and in your own IServiceFactory implementation, you may get the service provider from RequestContext.Features; then you may create the service instances using DI. See JsonRpc.AspNetCore.HttpContextServiceFactory for an example implementation.

Configure the services

First of all, we need to put all the service classes together, and do some basic configurations, so that it can actually process the RequestMessages, and return the expected ResponseMessages. 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.

Listen for the requests

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 Streams/TextReader/TextWriters
  • 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.");
}

Test the server

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"}
Clone this wiki locally