Udostępnij za pośrednictwem


WCF Extensibility – Transport Channels – Request channels, part 2

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

This is the part 2 of 3 of a “mini-series” inside the main series. For the other parts, here’s the list.

Continuing where I left off on the previous post, let’s jump right back into the scenario. This post, although having “Transport Channels” in the title, is more about the WCF Runtime pieces required for the scenario to work (I’ll finish the transport with asynchronous support in the next post), but I think it’s interesting to see that we don’t have to change anything in the transport to provide a better experience for the client. You can think of this as a review for most of the runtime pieces (at least from the client side).

Real world scenario: consuming a JSON-RPC service

What we ended up was a simple transport channel which could make synchronous requests to the service, but we needed to create the message ourselves. Before we get back to the transport channel, let’s make the client a little more user-friendly, by implementing the pieces needed to let us use a typed proxy to call the service.

And just to get it out of the way, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). I really kept the error checking to a minimum (especially in the “simple service” project and in many other helper functions whose inputs should be sanitized), to make the sample small.

Back to code. What we want in the end of the post is to be able to use an interface like the one below to call the operations in the service. That looks just like a “normal” WCF service contract.

  1. [ServiceContract]
  2. public interface ITypedTest
  3. {
  4.     [OperationContract]
  5.     int Add(int x, int y);
  6.     [OperationContract]
  7.     int Subtract(int x, int y);
  8.     [OperationContract]
  9.     int Multiply(int x, int y);
  10.     [OperationContract]
  11.     int Divide(int x, int y);
  12. }

The main component we need for this is a message formatter, more specifically an IClientMessageFormatter implementation. That’s the component which knows how to convert between the Message object and the typed CLR parameters for the operations. So let’s start with it. In order to deal with the JSON, I’m using the NewtonSoft Json.NET library, a popular JSON library in the community. Since all JSON-RPC requests have an ID, and this ID must be unique per session, I’m using a simple counter which gets incremented every time a new request gets made.

To serialize the request, the code first creates the wrapper JObject instance to which we add the method (operation) name, the parameters (serializing the objects using the Json.NET serializer) and the request id. We then call a helper method to serialize the JObject into a Message object (more on the helper in a moment). To deserialize the reply, we again use the helper to convert from the message object into a JObject instance, then we use the serializer to extract the result of the operation.

  1. class JsonRpcMessageFormatter : IClientMessageFormatter
  2. {
  3.     private OperationDescription operation;
  4.     private static int nextId = 0;
  5.  
  6.     public JsonRpcMessageFormatter(OperationDescription operation)
  7.     {
  8.         this.operation = operation;
  9.     }
  10.  
  11.     public object DeserializeReply(Message message, object[] parameters)
  12.     {
  13.         JObject json = JsonRpcHelpers.DeserializeMessage(message);
  14.         return JsonConvert.DeserializeObject(
  15.             json[JsonRpcConstants.ResultKey].ToString(),
  16.             this.operation.Messages[1].Body.ReturnValue.Type);
  17.     }
  18.  
  19.     public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
  20.     {
  21.         JObject json = new JObject();
  22.         json.Add(JsonRpcConstants.MethodKey, this.operation.Name);
  23.         JArray methodParams = new JArray();
  24.         json.Add(JsonRpcConstants.ParamsKey, methodParams);
  25.         for (int i = 0; i < parameters.Length; i++)
  26.         {
  27.             methodParams.Add(null);
  28.         }
  29.  
  30.         foreach (MessagePartDescription part in this.operation.Messages[0].Body.Parts)
  31.         {
  32.             object paramValue = parameters[part.Index];
  33.             if (paramValue != null)
  34.             {
  35.                 methodParams[part.Index] = JToken.FromObject(paramValue);
  36.             }
  37.         }
  38.  
  39.         json.Add(JsonRpcConstants.IdKey, new JValue(Interlocked.Increment(ref nextId)));
  40.         return JsonRpcHelpers.SerializeMessage(json, null);
  41.     }

The serialize / deserialize helpers are shown below. The serialize method writes out the JObject instance into a bytes, then creates a “raw” message based on those bytes. It also stores the original JObject instance in the message properties – this will be useful in the next runtime piece. Also, the serialize method will copy information from any previous message. Again, not very interesting at the client formatter level, but will be useful in the future. The deserialize method first checks if the message properties contain an instance of the JObject used to create it, thus short-circuiting the evaluation if it had already been done. If not, it will read the “raw” message into an array of bytes, then load it using the Json.NET classes into a new JObject instance.

  1. public static Message SerializeMessage(JObject json, Message previousMessage)
  2. {
  3.     using (MemoryStream ms = new MemoryStream())
  4.     {
  5.         using (StreamWriter sw = new StreamWriter(ms))
  6.         {
  7.             using (JsonTextWriter jtw = new JsonTextWriter(sw))
  8.             {
  9.                 json.WriteTo(jtw);
  10.                 jtw.Flush();
  11.                 sw.Flush();
  12.                 Message result = Message.CreateMessage(MessageVersion.None, null, new RawBodyWriter(ms.ToArray()));
  13.       ��         if (previousMessage != null)
  14.                 {
  15.                     result.Properties.CopyProperties(previousMessage.Properties);
  16.                     result.Headers.CopyHeadersFrom(previousMessage.Headers);
  17.                     previousMessage.Close();
  18.                 }
  19.  
  20.                 result.Properties[JsonRpcConstants.JObjectMessageProperty] = json;
  21.                 return result;
  22.             }
  23.         }
  24.     }
  25. }
  26.  
  27. public static JObject DeserializeMessage(Message message)
  28. {
  29.     if (message.Properties.ContainsKey(JsonRpcConstants.JObjectMessageProperty))
  30.     {
  31.         return (JObject)message.Properties[JsonRpcConstants.JObjectMessageProperty];
  32.     }
  33.     else
  34.     {
  35.         JObject json = null;
  36.         byte[] bytes = null;
  37.         using (XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents())
  38.         {
  39.             bodyReader.ReadStartElement("Binary");
  40.             bytes = bodyReader.ReadContentAsBase64();
  41.         }
  42.  
  43.         using (MemoryStream ms = new MemoryStream(bytes))
  44.         {
  45.             using (StreamReader sr = new StreamReader(ms))
  46.             {
  47.                 using (JsonTextReader jtr = new JsonTextReader(sr))
  48.                 {
  49.                     json = JObject.Load(jtr);
  50.                 }
  51.             }
  52.         }
  53.  
  54.         if (json == null)
  55.         {
  56.             throw new ArgumentException("Message must be a JSON object");
  57.         }
  58.  
  59.         return json;
  60.     }
  61. }

And the formatter is done. Next step: since we need to have a correlation between a request and a response in the JSON-RPC messages, we need to validate that the response “id” is the same as the request “id”. Also, the response may contain an error, and we want to surface that error as an exception to the client. For that, let’s use a message inspector (or more specifically, an IClientMessageInspector). As the request is about to be sent, the inspector stores the request id and returns it; that value will be then passed as a parameter (correlationState) to the AfterReceiveReply method. In that method the code retrieves the id of the reply, then validates that it’s the same as the request id, throwing an exception otherwise. That method also checks whether there is an error in the reply. If so, it convers it into an exception and throws it to the client. Notice that we use the message property “cache” shown in the formatter / helper in this method as well.

  1. class JsonRpcClientMessageInspector : IClientMessageInspector
  2. {
  3.     public void AfterReceiveReply(ref Message reply, object correlationState)
  4.     {
  5.         JObject json = GetJObjectPreservingMessage(ref reply);
  6.         int replyId = json["id"].Value<int>();
  7.         int requestId = (int)correlationState;
  8.         if (replyId != requestId)
  9.         {
  10.             throw new CommunicationException("Reply does not correspond to the request!");
  11.         }
  12.  
  13.         if (json[JsonRpcConstants.ErrorKey].Type != JTokenType.Null)
  14.         {
  15.             throw new CommunicationException("Error from the service: " + json[JsonRpcConstants.ErrorKey].ToString());
  16.         }
  17.     }
  18.  
  19.     public object BeforeSendRequest(ref Message request, IClientChannel channel)
  20.     {
  21.         JObject json = GetJObjectPreservingMessage(ref request);
  22.         return json["id"].Value<int>();
  23.     }
  24.  
  25.     static JObject GetJObjectPreservingMessage(ref Message message)
  26.     {
  27.         JObject json;
  28.         if (message.Properties.ContainsKey(JsonRpcConstants.JObjectMessageProperty))
  29.         {
  30.             json = (JObject)message.Properties[JsonRpcConstants.JObjectMessageProperty];
  31.         }
  32.         else
  33.         {
  34.             json = JsonRpcHelpers.DeserializeMessage(message);
  35.             message = JsonRpcHelpers.SerializeMessage(json, message);
  36.         }
  37.  
  38.         return json;
  39.     }
  40. }

Now we need something to hook the formatter and inspector to the client runtime. Time then for an enpdoint behavior, which is shown below. A message inspector is added to the client runtime, and for all operations in the contract, if it’s now an “untyped” operation (like we had before), we add our formatter to it.

  1. public class JsonRpcEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  4.     {
  5.         clientRuntime.MessageInspectors.Add(new JsonRpcClientMessageInspector());
  6.         foreach (OperationDescription operation in endpoint.Contract.Operations)
  7.         {
  8.             if (!JsonRpcHelpers.IsUntypedMessage(operation))
  9.             {
  10.                 ClientOperation clientOperation = clientRuntime.Operations[operation.Name];
  11.                 clientOperation.SerializeRequest = true;
  12.                 clientOperation.DeserializeReply = true;
  13.                 clientOperation.Formatter = new JsonRpcMessageFormatter(operation);
  14.             }
  15.         }
  16.     }
  17. }

And now we can use that nice typed interface to call our simple service:

  1. ChannelFactory<ITypedTest> typedFactory = new ChannelFactory<ITypedTest>(binding, address);
  2. typedFactory.Endpoint.Behaviors.Add(new JsonRpcEndpointBehavior());
  3. ITypedTest typedProxy = typedFactory.CreateChannel();
  4.  
  5. Console.WriteLine("Calling Add");
  6. int result = typedProxy.Add(5, 8);
  7. Console.WriteLine(" ==> Result: {0}", result);
  8. Console.WriteLine();
  9.  
  10. Console.WriteLine("Calling Multiply");
  11. result = typedProxy.Multiply(5, 8);
  12. Console.WriteLine(" ==> Result: {0}", result);
  13. Console.WriteLine();
  14.  
  15. Console.WriteLine("Calling Divide (throws)");
  16. try
  17. {
  18.     result = typedProxy.Divide(5, 0);
  19.     Console.WriteLine(" ==> Result: {0}", result);
  20. }
  21. catch (Exception e)
  22. {
  23.     Console.WriteLine("Error: {0}", e);
  24. }

And that’s the end of the detour through the WCF runtime in the post about channels.

Coming up

Back to the transport channel, we’ll add support for asynchronous calling on it. Be prepared for a lot of asynchronous spaghetti. You’ve been warned.

[Code in this post]

[Back to the index]