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
- Anonymous
August 24, 2010
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). - Anonymous
September 11, 2010
The comment has been removed - Anonymous
September 21, 2010
@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). - Anonymous
September 21, 2010
@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). - Anonymous
October 20, 2010
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. - Anonymous
October 31, 2010
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; } - Anonymous
December 08, 2010
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. - Anonymous
December 09, 2010
The comment has been removed - Anonymous
December 12, 2010
The comment has been removed - Anonymous
December 16, 2010
The comment has been removed - Anonymous
March 18, 2011
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 likehttp://localhost.:39330/MyService.svc/SaveApproval/0with 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 headersHTTP/1.1 201 CreatedServer: ASP.NET Development Server/10.0.0.0Date: Fri, 18 Mar 2011 20:37:36 GMTX-AspNet-Version: 4.0.30319Location: http://localhost.:39330/LabelApprovalService.svc/SaveApproval/64Cache-Control: privateContent-Length: 0Connection: CloseNotice the 201 status and the Location Header which says the ID of the newly created record is 64. - Anonymous
June 07, 2011
Found out nice article describing PUT and DELETEvordoom.com/.../Consume-REST-services-in-Silverlight.aspx - Anonymous
March 01, 2012
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. - Anonymous
August 13, 2012
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