Goran F
Goran F

Reputation: 103

Web Api 2 with OData v4 - Bound function returning complex object

In this simple example I am trying to get an object serialized as JSON from a Web Api 2 + OData v4 service. Controller has bound function Test which is returning an array of annon. objects.

public class ProductsController : ODataController
{
    [HttpGet]
    public IHttpActionResult Test(int key)
    {
        var res = new[]
        {
            new { Name = "a", Value = new[] { 1, 2, 3 } },
            new { Name = "b", Value = new[] { 2, 4, 5 } }

            // this also produces same result
            // new { Name = "a", Value = "c" },
            // new { Name = "b", Value = "c" }
        };

        return this.Ok(res);
    }
}

Edm is built with this piece of code:

ODataModelBuilder builder = new ODataConventionModelBuilder();

builder.EntitySet<Product>("Products");
var productType = builder.EntityType<Product>();

var f = productType.Function("Test").Returns<object>();

when I make a request to the service (eg. http://localhost:9010/odata/Products(33)/Default.Test) I am getting a strange response - an array of two empty objects, like this:

{
  "@odata.context": "http://localhost:9010/odata/$metadata#Collection(System.Object)",
  "value": [
    {},
    {}
  ]
}

In my real app I'm returning object serialized to a JSON string with Newtonsoft's Json converter - that works fine, but this problem is still bothering me. I suspect it is something related to OData's default serializer, but it is unclear to me how to configure it.

So, is it possible to configure edm function's return parameter in such way where I would get correctly serialized complex object?

Thanks!

Upvotes: 1

Views: 5941

Answers (2)

Chris Schaller
Chris Schaller

Reputation: 16554

Whilst it is true that working with dynamic responses is tricky, it's not that hard and you certainly do not need to resort to returning your objects through string encoding.

The key is that a dynamic response means that we can't use the standard EnableQueryAttribute to apply specific projections or filtering on the method response, and we can't return OkNegotiatedContentResult as this response object is designed to enable the runtime to manipulate how the response object is serialized into the HTTP response.

ApiController.Ok(T content);
Creates an System.Web.Http.Results.OkNegotiatedContentResult with the specified values.
content: The content value to negotiate and format in the entity body.

Content Negotiation
Content Negotiation is basically a mechansim to encapsulate the process to determine how your method response should be transmitted over http as well as the heavy lifting to physically encode the response.

By using Content Negotiation, your method only needs to return a query or raw c# object, even if the caller specified in the request that the output should be XML (instead of the standard JSON). The concept of dealing with the physical serialization and the logic to interpret the caller's intent is abstracted away so you do not need to worry about it at all.

There are 2 options that are available to you to customise the output:

  1. ApiController.JsonResult(T content);
    This allows you to specify the object graph to serialise, this will not respond to EnableQueryAttribute or Content Negotiation.

     return this.JsonResult(res);
    
    • This response is not in the usual OData Response wrapper so it does not include the standard @odata attributes like @odata.context.
    • If your response object does not match the type specified in the Action/Function defintion in the OData Model, calls to this endpoint will result in a But if the response object does not match the type specified in the OData Model this will result in a HTTP 406 Not Acceptable, so make sure you register the response an an object or another type of interface that your response object inherits from or implements.
  2. HttpResponseMessage
    Bypass OData response management all together and return HttpResponseMessage directly from your method. In this way you are responsible for serializing the response content as well as the response headers.

    This however bypasses all the OData mechanisms including response validation and formatting, meaning you can return whatever you want.

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(res))
    };
    result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
    return result;
    

Upvotes: 1

goroth
goroth

Reputation: 2600

As lukkea said, OData is not designed to work with anonymous types.
Side note, in you WebApiConfig you should change "Returns" to "ReturnsCollection" if you are returning a collection.

Anyway, let's assume you wrote the following.

return this.Ok(Newtonsoft.Json.JsonConvert.SerializeObject(res));

var f = productType.Function("Test").Returns<string>();

You would get back the following:

{
    "@odata.context": "http://localhost/Test/odata/$metadata#Edm.String",
    "value": 
        "[
            {\"Name\":\"a\",\"Value\":[1,2,3]},
            {\"Name\":\"b\",\"Value\":[2,4,5]}
        ]"
}

Note that there is still 2 items in the array but this time they are not empty.
Since OData did not know the return type in your previous example, it return 2 objects without values.

You have 2 options.

  1. Return back the anonymous type as a serialized json string and then deserialize that json on the client side.
  2. Create a class and return that type.

Option 1

// ON SERVER
return this.Ok(Newtonsoft.Json.JsonConvert.SerializeObject(res));

var f = productType.Function("Test").Returns<string>();

// ON CLIENT
string jsonString = odataContext.Products.ByKey(33).Test().GetValue();  
var objectList = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(jsonString);  

string firstObjectName = objectList[0].Name;

Option 2

// ON SERVER  
public class TestObject
{
    public string Name { get; set; }
    public List<int> Integers { get; set; }
}

var res = new List<TestObject>
{
     new TestObject { Name = "a", Integers = new List<int> { 1, 2, 3 } },
     new TestObject { Name = "b", Integers = new List<int> { 2, 4, 5 } }
};

return this.Ok(res);  

var f = productType.Function("Test").ReturnsCollection<TestObject>();

If you want to return a person with an extra property that is not strongly typed then you want ODataOpenType

Upvotes: 5

Related Questions