Reputation: 103
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
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:
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);
@odata
attributes like @odata.context
.object
or another type of interface that your response object inherits from or implements.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
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.
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