The Request-Response pattern seems somewhat simple on the surface; send a request, then wait for and receive a response that matches up with the original request.
However, implementing this pattern in an efficient manner is something that is easy to get wrong.
I’ve found that much of the material online regarding this topic tends to be either much too abstract, with no concrete examples provided, or it is much too verbose with examples that are very hard to follow along with or relate to in a meaningful way.
In this article, I will walk you through a working .NET solution using C# to implement the Request-Response pattern. I will use a realistic scenario to help convey the solution in a relatable way.
Example scenario
I’m going to show you how a specific RPC (Remote Procedure Call) protocol (JSON-RPC) and a transport protocol (WebSockets) can be used together to provide a realistic demonstration of the Request-Response pattern.
With JSON-RPC being an RPC protocol and WebSockets being a transport protocol, they are completely separate concepts. However, as you’ll see shortly, they can work together rather wonderfully, given their particular attributes.
Our mission is to implement the Request-Response pattern such that we can transmit a JSON-RPC request message over a WebSocket connection and subsequently receive the JSON-RPC response message, matching it up to the waiting request.
The key to matching up a request with a response is to define a unique ‘Correlation ID’ (i.e. Request ID) which is used to join the messages together. We’ll see how this works in more detail whenever we reach the code walkthrough section.
Protocols
Before diving into the code, let’s take a look at the protocols we’ll be using in the code samples.
JSON-RPC
JSON-RPC is a very light-weight RPC protocol that uses JSON to describe the format of messages which are transmitted between client and server. Being transport-agnostic, it can be used for a wide variety of purposes and works great when it is transmitted over WebSockets, thanks to its small payload.
For our solution, we’ll be using version 2.0 of the JSON-RPC protocol.
Here’s an overview of JSON-RPC for reference, taken from the official specification webpage.
JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol… It is transport agnostic in that the concepts can be used within the same process, over sockets, over http, or in many various message passing environments. It uses JSONÂ (RFC 4627) as data format.
It is designed to be simple!
JSON-RPC is a natural evolution of the XML-RPC protocol.
JSON-RPC has been a standard for some time now and as it is based on JSON you can create JSON schemas to document any APIs that you define.
WebSockets
WebSockets is a transport protocol that allows two-way (full-duplex) communication between a client and a server. It has been stable since 2011 and opens up lots of new possibilities compared to the traditional HTTP client to server approach where requests are always initiated from the client.
Here’s the abstract from the official IETF RFC webpage.
The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code. The security model used for this is the origin-based security model commonly used by web browsers. The protocol consists of an opening handshake followed by basic message framing, layered over TCP. The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that does not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or <iframe>s and long polling).
WebSockets is a very efficient protocol and can offer millisecond responses thanks to the fact that the connection is persistent (i.e. remains open) and the compact message format that is employed.
Code walkthrough
Let’s move on and look at the C# code which is required to implement the Request-Response pattern using JSON-RPC and WebSockets.
Note that a fully working version of all of the code I’m about to share with you can be found on my GitHub repository.
NuGet packages
I’m going to make use of two popular NuGet packages, namely JSON-RPC.NET and WebSocketSharp.
JSON-RPC.NET is a high-performance JSON-RPC server framework and it includes classes that we can use when serializing and deserializing JSON-RPC messages on the client.
WebSocketSharp is a WebSocket server and has client components too. It allows us to use WebSockets without requiring the client or server to be on a specific version of Windows which includes WebSocket protocol support.
Note that I’m also making use of Serilog for logging.
Solution overview
The code I’m going to show you focuses on a custom JsonRpcClient
class which uses a WebSocket
connection to send requests and receive responses.
The JsonRpcClient
class uses a dictionary to track the Correlation/Request ID for each request and the associated pending response. The dictionary uses a string
key to hold the Request ID and it uses a TaskCompletionSource
with a string
result property for the response value.
A naive approach to waiting for the response would be to use a loop and sleep for a small period of time on each loop iteration until the response has been received. However, this would be both ugly and horribly inefficient. TaskCompletionSource
offers us a much cleaner and efficient solution.
With TaskCompletionSource
we can store a reference to a Task
. We can choose when to start the Task
and we can subsequently mark the Task
as completed. This gives us a lot of control, allowing us to send a request and then wait for the server to respond before completing the Task
.
If you haven’t come across TaskCompletionSource
before, hopefully, this concept will make more sense after you see the code which uses it in action further below.
You can read more about TaskCompletionSource
in the Microsoft Docs.
Setting up the client
The JsonRpcClient
class mentioned in the previous section is just a regular public
class and its constructor accepts a WebSocket
instance, as per the sample code below.
/// <summary> /// Constructor. /// </summary> /// <param name="webSocket">The WebSocket channel to use</param> public JsonRpcClient(WebSocket webSocket) {     _webSocket            = webSocket;     _webSocket.OnMessage += ProcessMessage; }
The constructor assigns the WebSocket
parameter value to a private readonly
field and wires up an event handler for the OnMessage
event.
The OnMessage
event will be fired by the WebSocketSharp framework every time the server sends a message to the client. This will allow us to receive the responses relating to the requests we’ll be making to the server.
We’ll look at how the ProcessMessage
method works further below.
Creating requests
In order to send a JSON-RPC request to the server, we need a way of constructing the JSON-RPC request object which will be serialized and transmitted over the WebSocket connection.
To achieve this, we can write a CreateRequest
method as per the code below.
/// <summary> /// Creates a JSON-RPC request for the specified method and parameters. /// </summary> /// <param name="method">The method name</param> /// <param name="parameters">The list of parameters to pass to the method</param> /// <returns><see cref="JsonRequest"/></returns> public JsonRequest CreateRequest(string method, object parameters) {     // Get the next available Request ID.     int nextRequestId = Interlocked.Increment(ref _requestId);     if (nextRequestId > MaximumRequestId)     {         // Reset the Request ID to 0 and start again.         Interlocked.Exchange(ref _requestId, 0);         nextRequestId = Interlocked.Increment(ref _requestId);     }     // Create and return the Request object.     var request = new JsonRequest(method, parameters, nextRequestId);     return request; }
JSON-RPC.NET contains a JsonRequest
class that we can make use of.
Before creating and returning the JsonRequest
object, the Increment
 method of the Interlocked
class is used to get the next available Request ID.
Note that _requestId
is a private static
instance field. By using the Interlocked
class methods we can access and update the Request ID safely from multiple threads.
Response tracking
The JsonRpcClient
class contains a Responses Dictionary named _responses
, as follows.
/// <summary> /// Used to keep track of server responses. /// </summary> private static readonly ConcurrentDictionary<string, TaskCompletionSource<string>> _responses     = new ConcurrentDictionary<string, TaskCompletionSource<string>>();
Although in practice an integer number is typically used for the Request ID when using JSON-RPC, the dictionary key is set to a string
in order to keep things generic. The JSON-RPC specification allows numbers or strings to be used as a Request ID.
The type of the TaskCompletionSource
result value is also a string
. It is intended to hold the JSON string received from the server which will be deserialized into a response object.
Note that the Responses Dictionary is a ConcurrentDictionary
which allows it to be accessed safely from multiple threads without requiring explicit locking.
Sending requests
Here’s the complete code for the SendRequest
method which sends the specified JSON-RPC request to the server over the WebSocket connection then waits for and returns the response to the caller.
/// <summary> /// Sends the specified request to the WebSocket server and gets the response. /// </summary>
/// <typeparam name="TResult">The type of the expected result object</typeparam> /// <param name="request">The JSON-RPC request to send</param> /// <param name="timeout">The timeout (in milliseconds) for the request</param>
/// <returns>The response result</returns> public TResult SendRequest<TResult>(JsonRequest request, int timeout = 30000) {     var tcs       = new TaskCompletionSource<string>();     var requestId = request.Id;     try     {         string requestString = JsonConvert.SerializeObject(request);         // Add the Request details to the Responses dictionary so that we have           // an entry to match up against whenever the response is received.         _responses.TryAdd(Convert.ToString(requestId), tcs);         // Send the request to the server.         Log.Verbose($"Sending request: {requestString}");         _webSocket.Send(requestString);         Log.Verbose("Finished sending request");         var task = tcs.Task;         // Wait here until either the response has been received,         // or we have reached the timeout limit.         Task.WaitAll(new Task[] { task }, timeout);         if (task.IsCompleted)         {             // Parse the result, now that the response has been received.             JsonResponse response = JsonConvert.DeserializeObject<JsonResponse>(task.Result);             string responseString = JsonConvert.SerializeObject(response);             Log.Verbose($"Received response: {responseString}");             // Throw an Exception if there was an error.             if (response.Error != null) throw response.Error;             // Return the result.             return JsonConvert.DeserializeObject<TResult>(                 Convert.ToString(response.Result),                 new JsonSerializerSettings                 {                     Error = (sender, args) => args.ErrorContext.Handled = true                 });         }         else // Timeout response.         {             Log.Error($"Client timeout of {timeout} milliseconds has expired, throwing TimeoutException");             throw new TimeoutException();         }     }     catch (Exception ex)     {         Log.Error(ex, "An error occurred.");         throw;     }     finally     {         // Remove the request/response entry in the 'finally' block to avoid leaking memory.         _responses.TryRemove(Convert.ToString(requestId), out tcs);     } }
There’s quite a bit going on in this method and this is the key area of the code which implements the Request-Response pattern, so let’s break it down into manageable chunks.
First of all, we set up a TaskCompletionSource
which is used to track the state of our request and which will ultimately hold the response which is returned to the caller.
Next, we serialize the JsonRequest
object to a JSON string so that we can send it across the wire.
Before sending the request to the server, we add a new item to our Responses Dictionary. We use the Request ID from the JsonRequest
object which was passed in as the key. A reference to the TaskCompletionSource
we have created is used as the value.
The JSON-RPC message is then sent to the WebSocket server as a string
.
After sending the request to the server, the WaitAll
method of the Task
class is used to wait until either the Task
has completed or the specified timeout value has been reached.
If the Task
has completed, the code deserializes the Result
of the Task
into a JsonResponse
object.
If the response contains an error, an exception is thrown (JSON-RPC.NET maps JSON-RPC errors nicely into a JsonRpcException
for us).
Otherwise, the Result
property of the JsonResponse
object is deserialized into the type of object expected by the caller and is returned to the same.
If the Task
didn’t complete, a TimeoutException
is thrown.
Lastly, in the finally
block, the request/response entry is removed from the Responses Dictionary to avoid leaking memory which is, of course, a crucial consideration!
Processing responses
When the WaitAll
method of the Task
class has been called in the SendRequest
method, the code is waiting at this point until the Task
which is held within the TaskCompletionSource
object in the Responses Dictionary has been marked as completed.
Meanwhile, the server will hopefully send the client a response over the WebSocket and this will be processed by the ProcessMessage
method which is shown below.
/// <summary> /// Processes messages received over the WebSocket connection. /// </summary> /// <param name="sender">The sender (WebSocket)</param> /// <param name="e">The Message Event Arguments</param> private void ProcessMessage(object sender, MessageEventArgs e) {     // Check for Pings.     if (e.IsPing)     {         Log.Verbose("Received Ping");         return;     }     Log.Debug("Processing message");     // Log when the message is Binary.     if (e.IsBinary)     {         Log.Verbose("Message Type is Binary");     }     Log.Verbose($"Message Data: {e.Data}");     // Parse the response from the server.     JsonResponse response = JsonConvert.DeserializeObject<JsonResponse>(         e.Data,         new JsonSerializerSettings         {             Error = (sender, args) => args.ErrorContext.Handled = true         });     // Check for an error.     if (response.Error != null)     {         // Log the error details.         Log.Error("Error Message: " + response.Error.message);         Log.Error("Error Code: "    + response.Error.code);         Log.Verbose("Error Data: "  + response.Error.data);     }     // Set the response result.     if (_responses.TryGetValue(Convert.ToString(response.Id), out TaskCompletionSource<string> tcs))     {         tcs.TrySetResult(e.Data);     }     else     {         Log.Error("Unexpected response received. ID: " + response.Id);     }     Log.Debug("Finished processing message"); }
This is the method that gets executed whenever the OnMessage
event is fired by WebSocketSharp.
The first few lines of the method checks for ‘Pings’ and logs some information regarding the message. In the case of a ping being received, the code simply logs a message and returns, as a ping message is simply used to check that the WebSocket connection is still alive.
The Data
property of the MessageEventArgs
parameter value contains the message data as a string. The code deserializes the data into a JsonResponse
object, then checks and logs error details, if there are any.
Lastly, the code checks if the Responses Dictionary contains a matching Request ID and if it does the result of the TaskCompletionSource
is set. This will mark the related Task
as completed and will allow the SendRequest
method to continue.
Proxies
Now that we’ve looked at the internals of the JsonRpcClient
class, let’s see how we can use some more specific abstractions for calling server JSON-RPC methods.
In order to make the implementation more useable, we can create a ‘proxy’ class that accepts a JsonRpcClient
instance. Below I have included what the constructor for the TodoServicesProxy
class looks like.
/// <summary> /// Constructor. /// </summary> /// <param name="client">The JSON-RPC client</param> public TodoServicesProxy(JsonRpcClient client) {     _client = client; }
The JsonRpcClient
parameter value which is passed into the constructor is assigned to a private readonly
field.
We can now implement an instance method that abstracts the API request to the server and hides all of the underlying implementation details from the caller.
/// <summary> /// Gets a collection of Todos. /// </summary> /// <param name="userId">The ID of the User to get Todos for (optional)</param> /// <returns>A collection of all available Todos</returns> public IEnumerable<Todo> GetTodos(int? userId = null) {     Log.Debug($"Getting Todos");     var request  = _client.CreateRequest("getTodos", new { userId });     var response = _client.SendRequest<IEnumerable<Todo>>(request);     Log.Debug($"Found {response.Count()} Todos");     return response; }
Here we can see how JsonRpcClient
makes it really easy to create and send a request to the server and get the appropriate type of response object back.
In the GetTodos
method we simply create the request by calling the CreateRequest
method with the appropriate parameters, then we send the request using the SendRequest
method and return the response.
Demo API call
Let’s wrap up the code walkthrough with a demonstration of all of the pieces working together.
// Connect to the WebSocket server. using var webSocket = new WebSocket("ws://localhost:4649/json-rpc"); webSocket.Connect(); // Create the JSON-RPC client. var client = new JsonRpcClient(webSocket); // Send the request and get the response. var proxy = new TodoServicesProxy(client); var todos = proxy.GetTodos(userId: 2);
After connecting to the WebSocket
server and creating the JsonRpcClient
instance, we can new up a proxy class e.g. TodoServicesProxy
and call one of the available RPC methods.
This approach works really well for abstracting away the underlying complexities of matching up requests with responses and allows the caller to make API calls in much the same way that they would for local methods.
Summary
In summary, we’ve discussed the fundamentals of the Request-Response pattern and how to implement it using C#.
We’ve used JSON-RPC in combination with WebSockets to demonstrate a realistic scenario which I trust you found interesting to follow along with.
I hope that you have found this article insightful and perhaps you’ll find a real-world use case for using JSON-RPC and WebSockets together.
The complete code from this article can be found on my GitHub repository. As a bonus, I have also implemented a WebSocket JSON-RPC server which can be used when testing the solution.
Comments
Peter Todorov
Quote: “After sending the request to the server, the WaitAll method of the Task class is used to wait until either the Task has completed or the specified timeout value has been reached.”
June 24, 2021This has been mentioned on several occasions, also in the comments in the code. Isn’t WaitAll supposed to return when ALL tasks are completed (faulted or not)? In your code, WaitAll will return after at least 30 seconds, i.e. when the timeout task completes.
Jonathan Crozier
Hi Peter, thanks for the comment.
Yes, the WaitAll method returns when all of the specified Tasks have completed.
In the sample code, I am only passing one Task to the WaitAll method, so in my scenario, this has the same effect as using the WaitAny method of the Task class. The second parameter of the WaitAll method overload that I am using is the timeout value in milliseconds before the method stops waiting for the Task(s) to complete.
June 24, 2021Peter Todorov
I can’t believe I overlooked that. Please feel free to remove my comment.
June 25, 2021Other than that, thanks for a clear and practical implementation, and explanation, of the pattern. I would appreciate a more “async-await-agnostic” approach, but this for sure is something everyone can play with and add according to requirements.
Jonathan Crozier
Thanks, Peter. You’re most welcome and I appreciate the positive feedback. Yes, it would be straightforward to adjust the implementation to use a different approach according to the specific requirements, as per your remarks.
June 25, 2021Christopher M. Topinka
Nice post. Wonder if I can get an opinion on if an implementation like this fits for a microservice architecture for processing Big Data and any thoughts around this line of thinking – https://blogs.windows.com/windowsdeveloper/2016/03/14/when-to-use-a-http-call-instead-of-a-websocket-or-http-2-0/.
Specifically I’m attempting to create a mime parsing service with MimeKit that would need to scale as part of a high volume concurrent data processing pipeline. Is this a good candidate for web sockets? It seems like most of the consideration are around full duplex rather than less overhead. I don’t expect caching to come into play. Re-parsing the same message will happen infrequently.
Thanks in advance!
October 2, 2021Jonathan Crozier
Thanks, Christopher.
It’s difficult to advise with certainty without seeing the overall architecture of the solution. The blog post you referenced seems to do a good job of breaking down the pros and cons of HTTP vs WebSockets. Having said that, you may also want to consider the gRPC protocol since it offers high performance and is well suited to microservice communication.
I have a few gRPC blog posts that may be helpful and there are plenty of other great resources out there on the topic.
Getting to grips with gRPC… A gentle introduction
How to consume a gRPC service using .NET Core
Protecting access to gRPC services with Auth0
Good luck with your implementation, it sounds like an exciting project!
October 2, 2021