none
Feedback on json with padding implementation RRS feed

  • General discussion

  •  

    Hi all,

    I'm fairly new to WCF and this is my first post so please be gentle.  One of the attractive features of WCF to me is the ability to define different endpoints on the same service that return different encodings.  The ability to offer my customers a SOAP, POX, and JSON endpoint all with the same code is very cool.  In my particular case being able to offer JSONP would also be very useful as a lot of my customers want to be able to use javascript to call my services from different domains.  I tried to use the example described here:

    http://msdn.microsoft.com/en-us/library/cc716898.aspx

    but that example is not available for whatever reason.  Since the description for the example does pretty much outline the approach used to return jsonp I decided to take a whack at my own implementation based on the description and other examples provided.  However, since I am very new to WCF I'm sure there is plenty of things that can be improved and I was hoping to get some feedback on what I've done and get suggestions for improvement.

    The basic approach was to write a custom MessageEncoder that would wrap the built-in JSON encoder with a javascript function name based on a passed in querystring parameter.

    The first thing we need is a MessageEncodingBindingElement and BindingElementExtensionElement so that we can define our own binding in the config file.  This code was based heavily on the GZip encoding example.  The MessageEncodingBindingElement wraps a WebMessageEncodingBindingElement which will be used to create the factory we need to get the built-in JSON encoder.

    using System;  
    using System.Xml;  
    using System.ServiceModel;  
    using System.Configuration;  
    using System.ServiceModel.Channels;  
    using System.ServiceModel.Configuration;  
    using System.ServiceModel.Description;  
     
    namespace JsonpEncoder {  
        class JsonpMessageEncodingElement : BindingElementExtensionElement {  
            public JsonpMessageEncodingElement() {  
            }  
     
            public override Type BindingElementType {  
                get { return typeof(JsonpMessageEncodingElement); }  
            }  
     
            public override void ApplyConfiguration(BindingElement bindingElement) {  
                base.ApplyConfiguration(bindingElement);  
                JsonpMessageEncodingBindingElement binding = (JsonpMessageEncodingBindingElement)bindingElement;  
            }  
     
            protected override BindingElement CreateBindingElement() {  
                JsonpMessageEncodingBindingElement bindingElement = new JsonpMessageEncodingBindingElement();  
                this.ApplyConfiguration(bindingElement);  
                return bindingElement;  
            }  
        }  
    }  
     

    using System;  
    using System.Xml;  
    using System.ServiceModel;  
    using System.Configuration;  
    using System.ServiceModel.Channels;  
    using System.ServiceModel.Configuration;  
    using System.ServiceModel.Description;  
    using System.Text;  
     
    namespace JsonpEncoder {  
        class JsonpMessageEncodingBindingElement : MessageEncodingBindingElement {  
            private WebMessageEncodingBindingElement _innerBindingElement;  
     
            internal WebMessageEncodingBindingElement InnerBindingElement {  
                get { return _innerBindingElement; }  
                set { _innerBindingElement = value; }  
            }  
     
            public JsonpMessageEncodingBindingElement()  
                : this(new WebMessageEncodingBindingElement()) {  
            }  
     
            JsonpMessageEncodingBindingElement(WebMessageEncodingBindingElement bindingElement) {  
                _innerBindingElement = bindingElement;  
            }  
     
            public override MessageVersion MessageVersion {  
                get { return _innerBindingElement.MessageVersion; }  
                set { _innerBindingElement.MessageVersion = value; }  
            }  
     
            public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context) {  
                if (context == null)  
                    throw new ArgumentNullException("context");  
     
                context.BindingParameters.Add(this);  
                return context.BuildInnerChannelFactory<TChannel>();  
            }  
     
            public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context) {  
                if (context == null)  
                    throw new ArgumentNullException("context");  
     
                context.BindingParameters.Add(this);  
                return context.BuildInnerChannelListener<TChannel>();  
            }  
     
            public override bool CanBuildChannelListener<TChannel>(BindingContext context) {  
                if (context == null)  
                    throw new ArgumentNullException("context");  
     
                context.BindingParameters.Add(this);  
                return context.CanBuildInnerChannelListener<TChannel>();  
            }  
     
            public override MessageEncoderFactory CreateMessageEncoderFactory() {  
                return new JsonpMessageEncoderFactory(this, _innerBindingElement.CreateMessageEncoderFactory());  
            }  
     
            public override BindingElement Clone() {  
                return new JsonpMessageEncodingBindingElement((WebMessageEncodingBindingElement)this.InnerBindingElement.Clone());  
            }  
     
            public override T GetProperty<T>(BindingContext context) {  
                return _innerBindingElement.GetProperty<T>(context) ?? context.GetInnerProperty<T>();  
            }  
     
        }  
    }  
     

    Next we need a custom MessageEncoderFactory to produce our jsonp encoder.  It's pretty straightfoward, it also wraps a MessageEncoderFactory created by the WebMessageEncodingBindingElement in the above code.

    using System;  
    using System.ServiceModel.Channels;  
     
    namespace JsonpEncoder {  
        class JsonpMessageEncoderFactory : MessageEncoderFactory {  
            private JsonpEncoder _encoder;  
            private MessageEncoderFactory _innerFactory;  
     
            public JsonpMessageEncoderFactory(JsonpMessageEncodingBindingElement bindingElement, MessageEncoderFactory innerFactory) {  
                _innerFactory = innerFactory;  
                _encoder = new JsonpEncoder(bindingElement,_innerFactory.Encoder);  
            }  
     
            public override MessageEncoder Encoder {  
                get { return _encoder; }  
            }  
     
            public override MessageVersion MessageVersion {  
                get { return _innerFactory.MessageVersion; }  
            }  
        }  
    }  
     

    Now we need the actual encoder to do the work.  Here again we are wrapping an inner encoder provided by the wrapped factory.  For everything but the WriteMessage method we are just calling the methods on the wrapped encoder.  The WriteMessage method has pretty much all the interesting code in it as it actually sticks the call to the javascript function based on the querystring parameter named callback.  It defaults to something if that value isn't present.  Since we are referencing the HttpContext that means we'll need to set AspNetCompatibilityRequirements to Allowed.  This was one of the things in particular I was hoping someone had a better way to do.

    using System;  
    using System.IO;  
    using System.ServiceModel.Channels;  
    using System.ServiceModel;  
    using System.Text;  
    using System.Web;  
     
    namespace JsonpEncoder {  
        class JsonpEncoder : MessageEncoder {  
            private JsonpMessageEncodingBindingElement _bindingElement;  
            private MessageEncoder _innerEncoder;  
     
            public JsonpEncoder(JsonpMessageEncodingBindingElement bindingElement, MessageEncoder innerEncoder) {  
                _bindingElement = bindingElement;  
                _innerEncoder = innerEncoder;  
            }  
     
            public override string ContentType {  
                get { return _innerEncoder.ContentType; }  
            }  
     
            public override string MediaType {  
                get { return _innerEncoder.MediaType; }  
            }  
     
            public override MessageVersion MessageVersion {  
                get { return _innerEncoder.MessageVersion; }  
            }  
     
            public override bool IsContentTypeSupported(string contentType) {  
                if (contentType.StartsWith("application/json")) {  
                    return true;  
                } else {  
                    return false;  
                }  
            }  
     
            public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType) {  
                return _innerEncoder.ReadMessage(buffer, bufferManager, contentType);  
            }  
     
            public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType) {  
                return _innerEncoder.ReadMessage(stream, maxSizeOfHeaders, contentType);  
            }  
     
            public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) {  
                ArraySegment<byte> arraySegment = _innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);  
                string messageString = Encoding.UTF8.GetString(arraySegment.Array, 0, arraySegment.Count);  
                bufferManager.ReturnBuffer(arraySegment.Array);  
                byte[] buffer = bufferManager.TakeBuffer(arraySegment.Count);  
                string callbackFunction;  
                if (HttpContext.Current.Request.QueryString["callback"] != "") {  
                    callbackFunction = HttpContext.Current.Request.QueryString["callback"];  
                } else {  
                    callbackFunction = "callback";  
                }  
     
                messageString = callbackFunction + "('" + messageString.Replace("'""\\'") + "');";  
                int count = Encoding.UTF8.GetBytes(messageString, 0, messageString.Length, buffer, 0);  
                return new ArraySegment<byte>(buffer, 0, count);  
            }  
     
            public override void WriteMessage(Message message, Stream stream) {  
                _innerEncoder.WriteMessage(message, stream);  
            }  
     
        }  
    }  
     

    Now we need a service to use the new encoder.  I just made up something very basic for demonstration purposes, but note the setting of AspNetCompatibilityRequirements.

    using System;  
    using System.ServiceModel;  
    using System.ServiceModel.Activation;  
    using System.ServiceModel.Web;  
    using System.Runtime.Serialization;  
     
    [ServiceContract()]  
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]  
    public class TestService  {  
        [OperationContract]  
        [WebGet]  
        public string Operation1(string val) {  
            return "Hello: " + val;  
        }  
     
        [OperationContract]  
        [WebGet]  
        public TestContract Operation2(string field1, string field2) {  
            TestContract ret = new TestContract();  
            ret.Field1 = field1;  
            ret.Field2 = field2;  
            return ret;  
        }  
    }  
     
    [DataContract]  
    public class TestContract {  
        string field1;  
        string field2;  
     
        [DataMember]  
        public string Field1 {  
            get { return field1; }  
            set { field1 = value; }  
        }  
        [DataMember]  
        public string Field2 {  
            get { return field2; }  
            set { field2 = value; }  
        }  

    Now we'll set up the config file to define different endpoints to the service.  Well setup one endpoint for jsonp using our custom binding and httptransport.

        <system.serviceModel> 
     
            <extensions> 
                <bindingElementExtensions> 
                    <add name="jsonpEncoding" type="JsonpEncoder.JsonpMessageEncodingElement, JsonpEncoder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> 
                </bindingElementExtensions> 
            </extensions> 
     
            <services> 
                <service name="TestService" behaviorConfiguration="DefaultServiceBehavior">  
                    <endpoint name="TestService" address="" contract="TestService" binding="webHttpBinding" behaviorConfiguration="DefaultEndpointBehavior"/>  
                    <endpoint name="TestServiceJSON" address="json" contract="TestService" binding="webHttpBinding" behaviorConfiguration="JSONEndpointBehavior"/>  
                    <endpoint name="TestServiceJSONP" address="jsonp" contract="TestService" binding="customBinding" bindingConfiguration="jsonpBinding" behaviorConfiguration="JSONPEndpointBehavior" /> 
                    <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding" /> 
     
                </service> 
            </services> 
            <bindings> 
                <customBinding> 
                    <binding name="jsonpBinding">  
                        <jsonpEncoding /> 
                        <httpTransport manualAddressing="true" /> 
                    </binding> 
                </customBinding> 
            </bindings> 
            <serviceHostingEnvironment aspNetCompatibilityEnabled="true">  
            </serviceHostingEnvironment> 
            <behaviors> 
                <serviceBehaviors> 
                    <behavior name="DefaultServiceBehavior">  
                        <serviceDebug includeExceptionDetailInFaults="true"/>  
                        <serviceMetadata httpGetEnabled="True"/>  
                    </behavior> 
                </serviceBehaviors> 
                <endpointBehaviors> 
                    <behavior name="DefaultEndpointBehavior">  
                        <webHttp /> 
                    </behavior> 
                    <behavior name="JSONEndpointBehavior">  
                        <enableWebScript /> 
                    </behavior> 
                    <behavior name="JSONPEndpointBehavior">  
                        <enableWebScript /> 
                    </behavior> 
                </endpointBehaviors> 
            </behaviors> 
        </system.serviceModel> 
     

    Finally we need an html page to call the service.  Again, something very very basic for demonstration.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
    <html xmlns="http://www.w3.org/1999/xhtml" > 
    <head> 
        <title>Jsonp Encoder Test</title> 
          
        <script type="text/javascript">  
            function callbackFunction(jsonstr) {  
               var obj = eval('(' + jsonstr + ')');  
               objobj = obj.d;  
               alert("Field1 = " + obj.Field1);  
               alert("Field2 = " + obj.Field2);  
            }  
        </script> 
          
        <script src="testservice.svc/jsonp/Operation2?field1=test1&field2=test2&callback=callbackFunction"></script> 
          
    </head> 
    <body> 
     
    </body> 
    </html> 
     

    So, there it is.  I'd love to hear suggestions on better ways to do things.  Going through this process taught me quite a bit and I know I have a lot more to go.

    Eric



    Eric
    Saturday, November 1, 2008 12:35 AM

All replies

  • This's pretty neat
    Though I'd lean towards using WebOperationContext in stead of HttpContext and turn off ASPCompaibilityMode as

    if (WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters["callback"] != "")

    {

    callbackFunction = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters["callback"];

    }

    Thursday, December 11, 2008 6:22 PM
  • Great implementation! A few suggestions:

    On JsonpEncoder.IsContentTypeSupported, some clients also send text/json (instead of application/json):

            public override bool IsContentTypeSupported(string contentType)  
            {  
                if (contentType.StartsWith("application/json") || contentType.StartsWith("text/json"))  
                {  
                    return true;  
                }  
                else 
                {  
                    return false;  
                }  
            }  
     

    On JsonpEncoder.WriteMessage, you should take a buffer that is at least as large as your final message. Also, the messageOffset (which is almost always 0, but not guaranteed to be) should be taken into account as well:

            public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)  
            {  
                ArraySegment<byte> arraySegment = _innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);  
                string messageString = Encoding.UTF8.GetString(arraySegment.Array, 0, arraySegment.Count);  
                bufferManager.ReturnBuffer(arraySegment.Array);  
                string callbackFunction;  
                if (HttpContext.Current.Request.QueryString["callback"] != "")  
                {  
                    callbackFunction = HttpContext.Current.Request.QueryString["callback"];  
                }  
                else 
                {  
                    callbackFunction = "callback";  
                }  
     
                messageString = callbackFunction + "('" + messageString.Replace("'""\\'") + "');";  
                byte[] buffer = bufferManager.TakeBuffer(messageString.Length + messageOffset);  
                int count = Encoding.UTF8.GetBytes(messageString, 0, messageString.Length, buffer, messageOffset);  
                return new ArraySegment<byte>(buffer, messageOffset, count);  
            }  
     
    Thursday, December 11, 2008 6:45 PM
  •  Great post. It has been an enormous help to me.

    Just something to watch out for if you are using non-standard character sets. We found that when using some character sets in our message (mainly characters with accents) the length of the un-encoded message was not enough to define the size of the buffer required for the encoded message. We had to use Encoder.GetByteCount(...).

    int bufferSize = Encoding.UTF8.GetByteCount(messageString);  
    byte[] buffer = bufferManager.TakeBuffer(bufferSize  + messageOffset);  
    int count = Encoding.UTF8.GetBytes(messageString, 0, messageString.Length, buffer, messageOffset);  
    return new ArraySegment<byte>(buffer, messageOffset, count);  
     
    Thursday, January 15, 2009 1:20 PM