jsonasp.net-web-api2odataasp.net-web-api-odataodata-v4

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!


Solution

  • 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