Returning JsonP

Topics: Web Api, jQuery
Mar 21, 2011 at 4:02 PM

We currently have the following setup:

A service project, which is returning json much in the way of the contacts resource sample in the download.

A client project (one of several proposed clients) calling said service.

However, when we call the following method: 

 [WebGet(UriTemplate = "")]
        public JsonArray Get()
        {
            TransitSpy.DataContext.TransitSpyContext context = new TransitSpyContext();
            var result = new JsonArray();
            foreach (var agency in context.Agencies)
            {
                var nextAgency = (dynamic)new JsonObject();
                nextAgency.Name = agency.Name;
                nextAgency.AgencyId = agency.AgencyId;
                result.Add(nextAgency);
            }
            return result;
        }

We get denied due to a cross site request error. Returning JSON-P would fix this problem but we are unsure of the best way to approach this (that's assuming JSON-P is the right approach to begin with).
Any ideas?
Many thanks,
Kevin
Mar 29, 2011 at 4:29 AM

1) Use JSON-P

2) Host the client code in same domain of your services... Maybe a FTP or SSH where they can publish their code in your infra

3) Some people are using a script (found one in PHP) in the middle. Not best solution but seems to work.

Can't help more. These are the options that I know but didn't use any of them.

Coordinator
Mar 29, 2011 at 6:14 AM
Edited Mar 29, 2011 at 6:14 AM

Actually I believe Kevin went and implemented a solution. The way to do it now is to use a custom processor that encodes the json properly in a javascript call. Kevin are you going to share?

:-)

Mar 29, 2011 at 3:20 PM

Sorry, should have done this earlier. Here is the extremely naive implementation we have. Note that we are creating a read-only service right now, the only verb we are supporting is GET.

using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel.Description;
using System.Text;
using System.Web;
using Microsoft.ServiceModel.Http;

namespace TransitSpy.Server
{
    public class JsonPProcessor : JsonProcessor
    {
        public JsonPProcessor(HttpOperationDescription operation, MediaTypeProcessorMode mode) 
            : base(operation,mode)
        {            
            
        }

        public override IEnumerable<string> SupportedMediaTypes
        {
            get
            {
                return new List<string> { "text/javascript" };
            }
        }

        public override void WriteToStream(object instance, System.IO.Stream stream, System.Net.Http.HttpRequestMessage request)
        {                        
            var sb = new StringBuilder();
            var queryString = HttpUtility.ParseQueryString(request.RequestUri.Query);
            var callback = queryString["callback"] ?? "callback";
            sb.Append( callback + "({\"results\":" + instance + "});");
            byte[] buffer = Encoding.UTF8.GetBytes(sb.ToString());
            stream.Write(buffer, 0, buffer.Length);
        }
    }
}

And here is the value processor:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Linq;

using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

using Microsoft.ServiceModel.Description;
using Microsoft.ServiceModel.Dispatcher;
using Microsoft.ServiceModel.Http;

namespace TransitSpy.Server
{
    public class JsonPValueConfiguration : HttpHostConfiguration, IProcessorProvider, IInstanceFactory
    {
        private readonly CompositionContainer container;

        public JsonPValueConfiguration(CompositionContainer container)
        {
            this.container = container;
        }

        public void RegisterRequestProcessorsForOperation(HttpOperationDescription operation, IList<Processor> processors, MediaTypeProcessorMode mode)
        {
            processors.Add(new FormUrlEncodedProcessor(operation, mode));
            processors.Add(new JsonProcessor(operation, mode));
        }

        public void RegisterResponseProcessorsForOperation(HttpOperationDescription operation, IList<Processor> processors, MediaTypeProcessorMode mode)
        {
            processors.ClearMediaTypeProcessors();
            processors.Add(new JsonPProcessor(operation, mode));
        }

        // Get the instance from MEF
        public object GetInstance(Type serviceType, InstanceContext instanceContext, Message message)
        {
            var contract = AttributedModelServices.GetContractName(serviceType);
            var identity = AttributedModelServices.GetTypeIdentity(serviceType);

            // force non-shared so that every service doesn't need to have a [PartCreationPolicy] attribute.
            var definition = new ContractBasedImportDefinition(contract, identity, null, ImportCardinality.ExactlyOne, false, false, CreationPolicy.NonShared);
            return this.container.GetExports(definition).First().Value;            
        }

        public void ReleaseInstance(InstanceContext instanceContext, object service)
        {
            // no op
        }
    }
}
Works like a charm for our current purposes.

Coordinator
Apr 3, 2011 at 11:14 PM

Looks great Kevin! Thanks for sharing.

Apr 4, 2011 at 2:18 AM

My teammate Mike deserves most of the credit, but thanks!

Apr 16, 2011 at 11:40 AM

I was able to successfully implement easyXDM to enable cross-domain support to a WCF Web project. This allowed me to keep my service intact (i.e. still serving up JSON instead of JSONP). The only drawback is that the client needs to use the easyXDM JavaScript library to make calls to the web service. However, the calls are very similar to jQuery's implementation of web service calls.

May 12, 2011 at 2:22 AM

I'm trying to do this in a MessageHandler (Preview 4).  But I can't get access to the request (query string) to find the appropriate callback.

    public class JsonPFormatter : MediaTypeFormatter {
        public JsonPFormatter() {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
        }

        public override object OnReadFromStream(Type type, System.IO.Stream stream, HttpContentHeaders contentHeaders) {
            var serializer = new JsonSerializer();
            using (var sr = new StreamReader(stream))
            using (var reader = new JsonTextReader(sr)) {
                var result = serializer.Deserialize(reader, type);
                return result;
            }
        }

        public override void OnWriteToStream(Type type, object value, System.IO.Stream stream, HttpContentHeaders contentHeaders, System.Net.TransportContext context) {

            var sb = new System.Text.StringBuilder();
            string jsonString = string.Empty;

            var serializer = new JsonSerializer();
            using (var sw = new StringWriter()) {
                using (var writer = new JsonTextWriter(sw)) {
                    serializer.Serialize(sw, value);
                    jsonString = sw.ToString();
                }
            }

            var queryString = HttpUtility.ParseQueryString(request.RequestUri.Query); // <-- How do I get access to query string?
            var callback = queryString["callback"] ?? "callback";
            sb.Append(callback + "({\"results\":" + jsonString  + "});");
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
            stream.Write(buffer, 0, buffer.Length);
            
        }
    }

Should I be doing this somewhere else?  Am I going about this wrong?

Coordinator
May 12, 2011 at 4:29 AM

Looks like a formatter :-)

Right now it is unfortunate we don't expose the request/response on the formatter though we are going to fix that. For now there is an extension method you can access to get to the request message. You will need to use the static operation context...

OperationContext.Current.RequestContext.RequestMessage.ToHttpRequestMessage() should return you the request.

In the future drops we are planning to move request to be a parameter.

Glenn

May 12, 2011 at 4:00 PM

I'm hoping you can provide me some guidance or provide an example.  Not out of laziness but I've been trying for a while and still can't figure it out, plus I'm not the brightest guy.
How can I get access to the request or the Query within the following override in a MediaTypeFormatter:

public override void OnWriteToStream(Type type, object value, System.IO.Stream stream, HttpContentHeaders contentHeaders, System.Net.TransportContext context) {
 // ....
}

I don't see any extension method available for OperationContext.Current.RequestContext.RequestMessage
Any help or suggestions would be greatly appreciated.

May 12, 2011 at 6:00 PM

Here's an example!

    public override void OnWriteToStream(Type type, object value, Stream stream, System.Net.Http.Headers.HttpContentHeaders contentHeaders, System.Net.TransportContext context)
    {
      var request = OperationContext.Current.RequestContext.RequestMessage.ToHttpRequestMessage();
     
      var jsonValue = (JsonValue)value;
      var queryString = HttpUtility.ParseQueryString(request.RequestUri.Query);
      var callback = queryString["callback"] ?? "callback";
      var responseContent = String.Format("{0}({1});", callback, jsonValue);
      var buffer = Encoding.UTF8.GetBytes(responseContent);
      stream.Write(buffer, 0, buffer.Length);
    }

Mario

May 12, 2011 at 6:05 PM

I used NuGet to load Preview 4 into a new Web App.  I don't have the method ToHttpRequestMessage() available.

Do I need to include another reference?

Coordinator
May 12, 2011 at 6:52 PM
Edited May 12, 2011 at 6:52 PM

You do (have the method), it is an extension method in the namespace: Microsoft.ApplicationServer.Http.Channels

 

 

May 12, 2011 at 7:14 PM

Thanks, I included: "using Microsoft.ApplicationServer.Http.Channels;" and I do see the method now.
Next I'll have to figure out why OperationContext.Current is null.  But I'll experiment for a while before I dirty up your discussions. 

Coordinator
May 12, 2011 at 7:18 PM

Where are you getting that error? It shouldn't be if you are hosted. It won't work in a unit-test though so you need to check if it is null, or wrap it with a service that accesses it.

We have filed a bug to allow you to access request/response in formatters, but the work has not been done yet.

May 12, 2011 at 8:04 PM

This is my formatter in it's entirety:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.ApplicationServer.Http;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.IO;
using System.ServiceModel;
using Microsoft.ApplicationServer.Http.Channels;

namespace WebAPIWebApp {

    public class JsonPFormatter : MediaTypeFormatter {
        public JsonPFormatter() {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
        }

        protected override bool OnCanReadType(Type type) {
            return false;
        }

        public override object OnReadFromStream(Type type, System.IO.Stream stream, HttpContentHeaders contentHeaders) {
            var serializer = new JsonSerializer();
            using (var sr = new StreamReader(stream)) {
                using (var reader = new JsonTextReader(sr)) {
                    var result = serializer.Deserialize(reader, type);
                    return result;
                }
            }
        }

        public override void OnWriteToStream(Type type, object value, System.IO.Stream stream, HttpContentHeaders contentHeaders, System.Net.TransportContext context) {

            var sb = new System.Text.StringBuilder();
            string jsonString = string.Empty;

            var serializer = new JsonSerializer();
            using (var sw = new StringWriter()) {
                using (var writer = new JsonTextWriter(sw)) {
                    serializer.Serialize(sw, value);
                    jsonString = sw.ToString();
                }
            }

            var request = OperationContext.Current.RequestContext.RequestMessage.ToHttpRequestMessage(); // <-- Exception: OperationContext.Current is null

            var queryString = HttpUtility.ParseQueryString(request.RequestUri.Query); 
            var callback = queryString["callback"] ?? "callback";
            sb.Append(callback + "({\"results\":" + jsonString + "});");
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
            stream.Write(buffer, 0, buffer.Length);

        }
    }

}
I get a NullReferenceException at the line commented.

May 18, 2011 at 8:16 PM

Can anyone else get access to the OperationContext.Current inside a MediaTypeFormatter (like the example above)?

Is there anything special I need to do in the Application_Start that might help give me access? My config currently looks like the following:

var config = (HttpHostConfiguration)HttpHostConfiguration.Create();
// Make my NewtonsoftJsonFormatter the first one so that it gets called before built in JsonFormatter
config.OperationHandlerFactory.Formatters.Insert(0, new NewtonsoftJsonFormatter());
config.OperationHandlerFactory.Formatters.Add(new JsonPFormatter());
config.AddMessageHandlers(typeof(UriFormatExtensionMessageChannel), typeof(LoggingChannel), typeof(ApiKeyVerificationChannel));

Action<Collection<HttpOperationHandler>> handlers = c => {
    c.Add(new CompressionHandler());
    c.Add(new EDErrorHandler());
};
config.AddResponseHandlers(handlers, (s, o) => true);
//config.SetErrorHandler<GlobalErrorHandler>();

Jul 21, 2011 at 12:36 PM

Did you get it working?

Jul 21, 2011 at 5:49 PM
Yes, the version we posted here works fine. It's based on an earlier CTP, so I won't vouch for whether it still does.

Sent from my iPhone

On Jul 21, 2011, at 8:36 AM, AlexOnAspDotNet <notifications@codeplex.com> wrote:

From: AlexOnAspDotNet

Did you get it working?

Jul 22, 2011 at 11:43 AM

The OperationContext.Current always seems to be null. It is thread static so each thread had a copy of it. OnWriteToStream, etc methods. Does the 

var request = OperationContext.Current.RequestContext.RequestMessage.ToHttpRequestMessage();

work for anyone? I am using version 0.3.0.0 of the Microsoft.ApplicationServer.Http assembly. Is it possible to access request/response in formatters in other ways now?

Coordinator
Jul 22, 2011 at 1:54 PM
Not yet, though we are working on allowing it to be passed in as a param. The work around now would be to use an operation handler which writes to the body.

Sent from my Windows Phone

From: jenolaszloo
Sent: 7/22/2011 4:43 AM
To: Glenn Block
Subject: Re: Returning JsonP [wcf:250545]

From: jenolaszloo

The OperationContext.Current always seems to be null. It is thread static so each thread had a copy of it. OnWriteToStream, etc methods. Does the

var request = OperationContext.Current.RequestContext.RequestMessage.ToHttpRequestMessage();

work for anyone? I am using version 0.3.0.0 of the Microsoft.ApplicationServer.Http assembly. Is it possible to access request/response in formatters in other ways now?

Jul 25, 2011 at 8:30 PM

Got it working. Will blog about it soon.

Alex

Jul 26, 2011 at 9:06 AM

Here it is:

http://blog.alexonasp.net/post/2011/07/26/Look-Ma-I-can-handle-JSONP-(aka-Cross-Domain-JSON)-with-WCF-Web-API-and-jQuery!.aspx

Alex

Coordinator
Jul 26, 2011 at 9:58 AM
Alex, I am speechless, this is awesome!

>
Coordinator
Jul 26, 2011 at 10:07 AM
By the way...

By order of the web api community manifesto (which I just made up) you are hereby ordered to contribute this to web api contrib :-)
>
Jul 26, 2011 at 10:59 AM

Just started a repo on GitHub + NuGet Package... should I cancel this?

Alex

Jul 27, 2011 at 8:44 AM

Thanks for the response. I tested the operation handler and it works ok. 

Coordinator
Jul 27, 2011 at 8:59 AM

Alex

Just joking...up to you. I think this would make a nice addition to web api contrib, which also has a nuget package, but compeltely up to you.

Jul 27, 2011 at 9:46 AM

NP, will add it to contrib.

Aug 3, 2011 at 9:53 AM

@Glenn: I've send the pull request.

Aug 4, 2011 at 4:08 PM
AlexOnAspDotNet wrote:

@Glenn: I've send the pull request.


There's one from February that's still pending.

Aug 9, 2011 at 5:48 AM

Alex, your website seems to be down.

Aug 9, 2011 at 7:28 AM

@cb55555 - it works. Don't see any issues.

Alex

Aug 10, 2011 at 7:37 PM

I'm currently using easyXDM, and I was thinking about switching to jQuery+JSONP. Then I realized that this isn't going allow cross-domain posting. I guess I have to stick with easyXDM. :(

Nov 5, 2011 at 12:09 AM

Hi,

Im trying to figure out how to get JSONP working using Web API. I followed Alex's post: http://blog.alexonasp.net/post/2011/07/26/Look-Ma-I-can-handle-JSONP-%28aka-Cross-Domain-JSON%29-with-WCF-Web-API-and-jQuery!.aspx

and I implemented the JsonpResponseHandler  as per the example. What I dont understand is where does this HttpHostConfiguration class come from? and how am I suppose to hook the handler from my Global.asax.

I saw that HttpHostConfiguration was also mentioned on the MIX session with all of its fluent glory but i cant find any mention of it in the classes that make up Web API library... So im guessing the HttpConfiguration is the class to use instead?

Looks like im missing something but i dont know what. Im using the latest version downloaded as a nuget package (preview 5) and i got the contrib project as well.

Any ideas would be greatly appreciated.

Thanks,

Yoav.

Coordinator
Nov 7, 2011 at 4:04 PM

Correct, you should be using HttpConfiguration (or WebApiConfiguration if you are using the Web API enhancements).

Daniel Roth

Nov 7, 2011 at 5:56 PM

Thanks Daniel,

Any chance for some sample code hooking up a handler like was previously shown with the HttpHostConfiguration?

Yoav.

Jan 25, 2012 at 2:53 PM
Edited Jan 25, 2012 at 3:05 PM

here's one i did earlier, using the forcejsonhandler that was pointed out in the 'look ma' article (excellent piece of work). works a treat too!

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.SetDefaultHttpConfiguration(new WebApiConfiguration() { EnableTestClient = true });

            WebApiConfiguration w = (WebApiConfiguration)routes.GetDefaultHttpConfiguration();

            w.MessageHandlers.Add(typeof(ForceJsonHandler));

            routes.MapServiceRoute<DataAsXml>("api/myspace/xml");
            routes.MapServiceRoute<DataAsJson>("api/myspace/json");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
        }

note: you'll need to add a default constructor. this returns json, not jsonp and works very well x-domain.