Consuming REST/JSON services in Silverlight 4

In the previous post, which talked about the new Web programming model support for SL4, I mentioned that the support for strongly-typed consumption of REST/JSON services wasn't available out of the box. This post will show how to plug additional code to WCF in Silverlight to enable that. It will make it on par with the desktop version of the web programming model, with the exception of the raw programming model (input/return of type System.IO.Stream), which can also be added based on the same principles as the JSON support.

The whole code for this sample can be found here, in the Silverlight Web Services Code Gallery repository (https://code.msdn.microsoft.com/Release/ProjectReleases.aspx?ProjectName=silverlightws&ReleaseId=4059). I'll only walk through some parts of the sample here, since there is a lot of boilerplate code which would make this post too large if I were to post everything.

The first component required is the WebMessageEncodingBindingElement. As with the desktop, it should support both JSON and XML services, not just one or the other. This implementation simply delegates XML messages to the TextMessageEncodingBindingElement. For JSON messages, incoming messages will be passed on to the stack as raw bytes, so that later (at the formatter), it will decode it using the System.Json classes. The outgoing messages are created as binary raw bytes by the formatter as well, and the encoder simply writes those bytes to the output.

public class WebMessageEncodingBindingElement : MessageEncodingBindingElement
{
...
class WebMessageEncoder : MessageEncoder
  {
MessageEncoder xmlEncoder = new TextMessageEncodingBindingElement(
MessageVersion.None, Encoding.UTF8).CreateMessageEncoderFactory().Encoder;
...
public override bool IsContentTypeSupported(string contentType)
{
return this.xmlEncoder.IsContentTypeSupported(contentType) ||
contentType.Contains("/json"); // text/json, application/json
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
if (this.xmlEncoder.IsContentTypeSupported(contentType))
{
return this.xmlEncoder.ReadMessage(buffer, bufferManager, contentType);
}
Message result = Message.CreateMessage(MessageVersion.None, null, new RawBodyWriter(buffer));
result.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Json));
return result;
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
bool useRawEncoder = false;
if (message.Properties.ContainsKey(WebBodyFormatMessageProperty.Name))
{
WebBodyFormatMessageProperty prop = (WebBodyFormatMessageProperty)message.Properties[WebBodyFormatMessageProperty.Name];
useRawEncoder = prop.Format == WebContentFormat.Json;
}
if (useRawEncoder)
{
MemoryStream ms = new MemoryStream();
XmlDictionaryReader reader = message.GetReaderAtBodyContents();
byte[] buffer = reader.ReadElementContentAsBase64();
byte[] managedBuffer = bufferManager.TakeBuffer(buffer.Length + messageOffset);
Array.Copy(buffer, 0, managedBuffer, messageOffset, buffer.Length);
return new ArraySegment<byte>(managedBuffer, messageOffset, buffer.Length);
}
else
{
return this.xmlEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
}
}
}

The RawBodyWriter class, used by the encoder, simply saves the buffer and writes it out in a single XML element:

class RawBodyWriter : BodyWriter
{
  ArraySegment<byte> buffer;
  public RawBodyWriter(ArraySegment<byte> buffer) : base(true)
{
    this.buffer = buffer;
}
  public RawBodyWriter(byte[] buffer) : base(true)
{
    this.buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
}
  protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
{
writer.WriteStartElement("Binary");
writer.WriteBase64(this.buffer.Array, this.buffer.Offset, this.buffer.Count);
writer.WriteEndElement();
}
}

To plug-in the formatter in the pipeline, we can create our own WebHttpBehavior subclass, and override the Get{Request/Reply}ClientFormatter methods. On the request formatter, we can determine based on the RequestFormat property of the [WebGet/WebInvoke] attribute for the operation which formatter to use (the one for JSON, or the default one for XML); on the reply formatter, this information is only known at the time when the response arrives, so we hold on to both formatters (the JSON and the XML ones) in a simple multiplexing formatter:

public class WebHttpBehaviorWithJson : WebHttpBehavior
{
  protected override IClientMessageFormatter GetRequestClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
{
    if (GetRequestFormat(operationDescription) == WebMessageFormat.Json)
{
      return new JsonClientFormatter(endpoint.Address.Uri, operationDescription, this.DefaultBodyStyle);
    }
    else
    {
      return base.GetRequestClientFormatter(operationDescription, endpoint);
}
}
  protected override IClientMessageFormatter GetReplyClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
{
    IClientMessageFormatter xmlFormatter = base.GetReplyClientFormatter(operationDescription, endpoint);
    IClientMessageFormatter jsonFormatter = new JsonClientFormatter(endpoint.Address.Uri, operationDescription, this.DefaultBodyStyle);
    return new JsonOrXmlReplyFormatter(xmlFormatter, jsonFormatter);
}
  WebMessageFormat GetRequestFormat(OperationDescription od)
{
    WebGetAttribute wga = od.Behaviors.Find<WebGetAttribute>();
    WebInvokeAttribute wia = od.Behaviors.Find<WebInvokeAttribute>();
    if (wga != null && wia != null)
{
      throw new InvalidOperationException("Only 1 of [WebGet] or [WebInvoke] can be applied to each operation");
}
    if (wga != null)
{
      return wga.RequestFormat;
}
    if (wia != null)
{
      return wia.RequestFormat;
}
    return this.DefaultOutgoingRequestFormat;
}
}
class JsonOrXmlReplyFormatter : IClientMessageFormatter
{
...
  public object DeserializeReply(Message message, object[] parameters)
{
    object prop;
    if (message.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out prop))
{
      WebBodyFormatMessageProperty format = (WebBodyFormatMessageProperty)prop;
      if (format.Format == WebContentFormat.Json)
{
        return this.jsonFormatter.DeserializeReply(message, parameters);
}
}
    return this.xmlFormatter.DeserializeReply(message, parameters);
}
}

The JsonClientFormatter uses the DataContractJsonSerializer and the System.Json classes to serialize and deserialize the parameters into / from JSON. Below is a snippet of the implementation of the DeserializeReply method. It's not the most efficient way to implement it, but it's simple enough that it shouldn't be a huge bottleneck for most applications.

  public object DeserializeReply(Message message, object[] parameters)
{
...
    XmlDictionaryReader reader = message.GetReaderAtBodyContents();
    byte[] buffer = reader.ReadElementContentAsBase64();
    MemoryStream jsonStream = new MemoryStream(buffer);
    WebMessageBodyStyle bodyStyle = GetBodyStyle(this.operationDescription);
    if (bodyStyle == WebMessageBodyStyle.Bare || bodyStyle == WebMessageBodyStyle.WrappedRequest)
{
      DataContractJsonSerializer dcjs = new DataContractJsonSerializer(this.operationDescription.Messages[1].Body.ReturnValue.Type);
      return dcjs.ReadObject(jsonStream);
}
    else
    {
      JsonObject jo = JsonValue.Load(jsonStream) as JsonObject;
      if (jo == null)
{
        throw new InvalidOperationException("Response is not a JSON object");
}
      for (int i = 0; i < this.operationDescription.Messages[1].Body.Parts.Count; i++)
{
        MessagePartDescription outPart = this.operationDescription.Messages[1].Body.Parts[i];
        if (jo.ContainsKey(outPart.Name))
{
parameters[i] = Deserialize(outPart.Type, jo[outPart.Name]);
}
}
      MessagePartDescription returnPart = this.operationDescription.Messages[1].Body.ReturnValue;
      if (returnPart != null && jo.ContainsKey(returnPart.Name))
{
        return Deserialize(returnPart.Type, jo[returnPart.Name]);
}
      else
      {
        return null;
}
}
}
  static object Deserialize(Type type, JsonValue jv)
{
    if (jv == null) return null;
    DataContractJsonSerializer dcjs = new DataContractJsonSerializer(type);
    MemoryStream ms = new MemoryStream();
jv.Save(ms);
ms.Position = 0;
    return dcjs.ReadObject(ms);
}

As mentioned in the beginning of this post, the full implementation can be found at the Code Gallery. Let us know if you think this is useful, depending on the number of responses we will consider including support out-of-the-box for JSON in a future Silverlight release.