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.

 

Comments (14)

  1. Lars Kemmann says:

    We're going to use the XML variant in our service for the time being; however, the JSON format is much more compact, and our needs (distributed Azure-hosted app with potentially large scalability demands) suggest that the more we can compress the data transmission, the better.

    JSON support would definitely be nice to have.

    More nice to have, though, would be additional mentions of this topic (i.e. the REST/POX support) anywhere on the Internets.  It seems like your blog post is the only mention of this; I couldn't find it anywhere else (not even MSDN).

  2. Cuthahotha says:

    Carlos, thanks for the post.  I've downloaded the code, and am receiving errors running it.

    I get "The provided URI scheme 'file' is invalid; expected 'http'.

    Parameter name: via"

    In the code line in side the CreateProxy

     IRestService proxy = factory.CreateChannel();

  3. Carlos Figueira says:

    @Cuthahota, the client tries to guess the server address by using the same address that was used to open the web page; if you opened the page (SLAppTestPage.html) from the disk directly (c:somethingSLApp.WebSLAppTestPage.html), then the client will "think" that the server is also to be accessed via the file server (which doesn't work) – see the GetServiceAddress function.

    To get it to work, either deploy the SLApp.Web directory in IIS, or run it within Visual Studio (it will use the VS dev web service).

  4. Carlos Figueira says:

    @Lars, what I've typically seen in SL applications which want to talk JSON with a service is that it will use a combination of the System.Json types (JsonValue/JsonArray/JsonObject/JsonPrimitive) to create/parse JSON in an untyped way, or the DataContractJsonSerializer (to convert between CLR types and JSON), and then use the WebClient/HttpWebRequest directly to make the calls. Until SL3 this was actually the only way to do that – but with the extensibility points added in SL4 it's actually possible use the same WCF programming model in the SL client as well.

    It's possible that this support may be included in future SL versions, although it would require many people asking for it (there's a big resistance in adding many features in Silverlight, to keep the download size as small as possible, which IMO is a good thing).

  5. borice says:

    Carlos, thank you for the helpful code.  How would the code you've posted need to be modified if I want to be able to add the request header  "Accept: application/json" to any service method that has ResponseFormat = Json ?

    Thank you.

  6. borice, in order to add the new header to the request, you'd use GetRequestClientFormatter to return an instance of a custom class (which implements IClientMessageFormatter), passing the original formatter used for the message. In the implementation of SerializeRequest, you would first call the original formatter to create the message object, and then set the Accept header in its properties, something similar to the code below:

           public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)

           {

               Message result = this.originalFormatter.SerializeRequest(messageVersion, parameters);

               HttpRequestMessageProperty reqProp;

               if (result.Properties.ContainsKey(HttpRequestMessageProperty.Name))

               {

                   reqProp = (HttpRequestMessageProperty)result.Properties[HttpRequestMessageProperty.Name];

               }

               else

               {

                   reqProp = new HttpRequestMessageProperty();

                   result.Properties.Add(HttpRequestMessageProperty.Name, reqProp);

               }

               reqProp.Headers[HttpRequestHeader.Accept] = "application/json";

               return result;

           }

  7. Theo says:

    Hi Carlos,

       Thanks for this wonderful post, can you let me know what change will i need to do if i want to use the same encoder with a normal WCF service instead of a RESTful service.

  8. Theo, for "normal" WCF service you don't need to use a special encoder – there's already support in Silverlight for consuming such services, as long as the endpoint uses a binding which is supported by Silverlight, such as BasicHttpBinding. You can simply use the equivalent SL bindings which are shipped out-of-the-box.

  9. Theo says:

    Thanks Carlos for your reply, I was not able to find which binding in Silverlight i need to use that supports JSON out-of-the-box. May I am looking at a worng place, can please you guide me to the right place where I can get the details or let me know which biding I can use in Silverlight that sends data in JSON.

  10. There's no out-of-the-box binding in SL which sends JSON data – which is one of the reasons for this post. Do you need the communication between WCF and SL to be in JSON? JSON is definitely more concise than XML, but in SL we also support the WCF binary encoding, which makes the messages quite small. You can create a custom binding using HTTP and Binary (which works on both WCF/Desktop and Silverlight):

    <customBinding>

      <binding name="BinaryOverHttp">

         <binaryMessageEncoding/>

         <httpTransport>

      </binding>

    </customBinding>

    Or in code:

      CustomBinding binding = new CustomBinding(

           new BinaryMessageEncodingBindingElement(),

           new HttpTransportBindingElement());

  11. Paul Cavacas says:

    Any suggestion on how I can read back a Location Header from the response.  I have a JSON service that does a POST to Insert/Update data.  If it does an insert then the Http header is written back with the ID of the record that was inserted,i.e.

    I send a request like

    http://localhost.:39330/MyService.svc/SaveApproval/0

    with a Post Body of

    {"ActionDate":null,"Comments":null,"DocumentId":360,"Id":0,"RoundId":219,"Status":null,"User":{"BP":null,"D":null,"Dep":null,"E":null,"FN":"Paul","LC":null,"LN":"smith","UN":"somebody"}}

    then the reply comes back with the following headers

    HTTP/1.1 201 Created

    Server: ASP.NET Development Server/10.0.0.0

    Date: Fri, 18 Mar 2011 20:37:36 GMT

    X-AspNet-Version: 4.0.30319

    Location: http://localhost.:39330/LabelApprovalService.svc/SaveApproval/64

    Cache-Control: private

    Content-Length: 0

    Connection: Close

    Notice the 201 status and the Location Header which says the ID of the newly created record is 64.

  12. Rick says:

    Found out nice article describing PUT and DELETE

    vordoom.com/…/Consume-REST-services-in-Silverlight.aspx

  13. Paul, if you want to read the response headers (including the Location header), you'll need to, prior to calling the EndXXX method, wrap the call in a new OperationContextScope. That way you'll be able to access the HTTP response headers using the HttpResponseMessageProperty (via OperationContext.Current.IncomingMessageProperties). Notice that you need to select the WebRequestCreator.ClientHttp for the web request, otherwise you won't have access to the response headers.

  14. Bhavesh says:

    Hello,

    I have a question. I have SL 5 project (Client and Web application). Now I need to call third party API which can return response in JSON or XML. My question is about from where to call this third party API? – From Client application or Web application in the SL 5 project…? Can you please guide.

    Thanks,

    Bhavesh