Returning Anonymous Types

Topics: Web Api
Apr 21, 2011 at 4:25 PM
Edited Apr 21, 2011 at 4:31 PM

The MVC way of doing this is something like return Json(new { product.Name, product.Price, ect });

Is this possible with the web api? The example below works however nothing is sent back to the client so i'm thinking the serialization isn't working on the object type.

       [Description("Returns a product catalog.")]
       [WebGet(UriTemplate = "")]
       public IQueryable<object> GetProducts()
{
var query = from product in AllProducts()
select new
{
ID = product.idProduct,
Name = product.Name,
Price = product.Price
};
return query.AsQueryable();
}
Coordinator
Apr 21, 2011 at 5:06 PM

Not in the way you are showing. JsonValue is a new type that you can use for returning typeless json. It is a dynamic object. If you look at our JsonValue sample you will see how to use it. You can accept/return JsonValue in your operations.

Glenn

Coordinator
Apr 21, 2011 at 5:08 PM

IQueryable<JsonValue> probably won't work, but just returning JsonValue will. Are you returning IQueryable because you want to use the OData uri format?

Apr 21, 2011 at 5:11 PM

I don't have any dependency on IQueryable at the moment so I'll give the JsonValue a shot. Where can I find that type by the way?

Apr 21, 2011 at 5:13 PM

NM. found it.

Apr 21, 2011 at 5:25 PM

If the return type is JsonValue what should I be converting the linq query into?



        
    
Coordinator
Apr 21, 2011 at 5:36 PM

We have a JsonArray object that you can use. You would take the query and populate a JsonArray.

Glenn

Coordinator
Apr 21, 2011 at 5:38 PM

If you use JsonArray, the return value signature should still be (today) JsonValue. See the JsonValue sample.

Apr 21, 2011 at 5:43 PM

Thanks for the help. I've looked through the sample but can't seem to find anything specific to using anonymous types. Even for the JsonArray I am bound to use JsonObjects which don't seem useful from a linq query.

Coordinator
Apr 21, 2011 at 5:45 PM

We don't support anonymous types. JsonValue however is dynamic so you can cast it to dynamic and just set properties on it.

Apr 21, 2011 at 5:57 PM

This compiles and runs but still no results being sent to the client. What am I missing?

                  public JsonValue GetProducts()
{
                        var results = new JsonArray();

                        foreach (var product in GetAllProducts())                         {                             dynamic jsonValue = new JsonObject();                             jsonValue.ID = product.idProduct;                             jsonValue.Name = product.Name;                             jsonValue.Price = product.Price;                             results.Add(jsonValue);                         }                         dynamic result = new JsonObject();                         result.Products = results;                         return result;
Coordinator
Apr 21, 2011 at 7:13 PM
Edited Apr 21, 2011 at 7:13 PM

Probably that you need to send accept headers, "application/json". How are you calling this? If you are using jQuery for example you need to call the getJSON method.

I created a sample for you that uses your scenario with the service being called from jQuery. You can get it here: http://cid-f8b2fd72406fb218.office.live.com/self.aspx/Public/JsonArraySample.zip

In terms of takeaways, it's kind of pointless for you to have to send the accept header if the service uses JsonValue as you've made an obivous choice of Json only. I'll look into that with the team.

Let me know if it works for you.

Apr 21, 2011 at 7:17 PM

One other thing. If I create a concrete class for this (e.g. ProductDTO) and send it as the response (public IQueryable<ProductDTO>) everything works fine. Sends XML or json based on client header. Also, the contents of the JsonArray are showing the json encoding when I do a ToString() on the object so there must be something it doesn't like about my objects once it gets around to sending the response.

Coordinator
Apr 21, 2011 at 7:21 PM

Can you check out my sample that I linked to above and tell me if there is anything you are doing differently, or you can just send me a repro showing what you are experiencing.

Thanks

Glenn

Coordinator
Apr 21, 2011 at 7:22 PM

In terms of multi-format, JsonValue is only for a single format. You can definitely use DTOs if you want to support multiple formats. If you look at our ContactManager example that is what we are doing. We return a collection of contacts that are represented in a range of different formats.

Apr 21, 2011 at 7:25 PM

When you mentioned the client header a light bulb went off. I'm not sending the json header, just a simple GET request from the browser which may indicate why I'm not getting any results back since it may require the client header to exist to operate (which I guess is the 'pointless' part you mention). So then, how would I get the fancy serialization based on client heades if only the JsonValue can be used for dynamic instances? Or have I gotten into uncharted territory. Thanks.

Coordinator
Apr 21, 2011 at 7:27 PM

Yeah that is probably it. How are you planning to "actually" call it though? For example if you look in my sample I linked to above, I am using jQuery. If you call the getJSON method it will do the right thing.

Glenn

Apr 21, 2011 at 7:31 PM

Yes I get the proper results if I send the json header from the client. We're an ISV so the api will be used externally. We want to let the caller determine the format so we need to support xml and other formats as well.

Coordinator
Apr 21, 2011 at 7:49 PM

If you need to support multiple results, I would NOT use JsonValue as it is not designed for that.

If you want to support anonymous types, though we don't, you "could" write a custom formatter that you plug in the middle which will allow you to serialize/deserialize any way you want to even to an anonymous type. It's just not something we support out of the box (currently).

Apr 21, 2011 at 7:56 PM

Ok I figured as much. A simple bit of object reflection should get me where I want to go.

Your support is very much appreciated!

Coordinator
Apr 21, 2011 at 8:04 PM

No worries, glad to help.

Depending on how you implement it, you will probably need to author an HttpOperationHandlerFactory. The factory has a collection of Formatters. If you want to create a general purpose one that supports say dynamic you will want to add it at the beginning of that collection. Then override the CanReadAsType / CanWriteAsType methods to that you can lock it down to only work for cases where the type is Object.

If you want to support true anonymous that might be a bit more work. One challenge is on the incoming side you can't expose an anoymous type as a receiving value. You can however have a dynamic/object param.

Good luck!

Glenn

Apr 21, 2011 at 8:12 PM
gblock wrote:

Probably that you need to send accept headers, "application/json". How are you calling this? If you are using jQuery for example you need to call the getJSON method.

I created a sample for you that uses your scenario with the service being called from jQuery. You can get it here: http://cid-f8b2fd72406fb218.office.live.com/self.aspx/Public/JsonArraySample.zip

In terms of takeaways, it's kind of pointless for you to have to send the accept header if the service uses JsonValue as you've made an obivous choice of Json only. I'll look into that with the team.

Let me know if it works for you.


Glenn, I noticed this, too.  If I have a service that returns JsonValue, in order to actually get JSON back using HttpClient, I still have to add an Accept header:

	httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Coordinator
Apr 21, 2011 at 8:26 PM

Yes because we implement server driven conneg based on accept headers, which is how the HTTP spec says to do it :-)

I agree though for JsonValue we should probably relax that.

Another way you can address this for now is to create a custom MessageHandler (channel). The handler can add the accept header automatically if it is not present. Because it's a low-level plugin, you write it once and are done.

Check out the UriExtension handler in the advanced sample to see how you can do it now.

Glenn

Apr 21, 2011 at 8:27 PM

Yeah I got bit by that too today.  If you don't, the default RequestContentHandler tries to serialize the JSONValue using the XMLSerializer!  I'm very tempted to remove that default behaviour, I'm just not sure what else it does.

Coordinator
Apr 21, 2011 at 8:29 PM

It's actually not there in the handler. It's the default formatter collection which gets populated with XML, Json, etc. You can clear that easily in a custom HttpOperationHandlerFactory.

Glenn

Apr 21, 2011 at 9:34 PM
gblock wrote:

It's actually not there in the handler. It's the default formatter collection which gets populated with XML, Json, etc. You can clear that easily in a custom HttpOperationHandlerFactory.

Glenn


Good tip, thanks.