Search code examples
asp.net-mvc-4asp.net-web-apiodata

Deserialize object content manually in Web-API OData


When using WCF Data Services Client to communicate with my OData API, I occasionally need to use the AddRelatedObject method of System.Data.Services.Client.DataServiceContext

This generates a POST to the endpoint that looks like:

http://base_url/odata/Entity(key)/NavigationProperty

The content of the http request is the serialized NavigationProperty entity.

Since a post to this URL is unmapped in a default odata controller, I wrote a new EntitySetRoutingConvention class:

Public Class AddRelatedObjectRoutingConvention
    Inherits EntitySetRoutingConvention

    Public Overrides Function SelectAction(odataPath As Http.OData.Routing.ODataPath, controllerContext As Http.Controllers.HttpControllerContext, actionMap As ILookup(Of String, Http.Controllers.HttpActionDescriptor)) As String
        If controllerContext.Request.Method = Net.Http.HttpMethod.Post Then
            If odataPath.PathTemplate = "~/entityset/key/navigation" Then
                If actionMap.Contains("AddRelation") Then
                    Return "AddRelation"
                End If
            End If
        End If
        Return Nothing
    End Function
End Class

I added this to the default routing conventions list and passed it to the MapODATARoute routine.

In my base controller I implemented AddRelation, and it is called perfectly.

From this I am able to get the odatapath, and parse it to determine the types and keys that are needed.

The problem I am having, is that once I know that my parent entity is a Tenant with a key of 1, and that the NavigationProperty of Users in the Tenant entity is a User entity, I can not figure out how to manually call an odatamediatypeformatter to deserialize the http content into the User entity so that I can then add it to the Tenants Users collection.

I have reviewed the OData source in an effort to find out how to the Web-API pipeline calls and deserializes the entity but have been unable to find the point at which the OData library takes the http content and turns it into an entity.

If anyone has an ideas to point me in the right direction, I will continue researching and update if I figure out more.

UPDATE 06/28/2013 Thanks to RaghuRam, I've been able to get closer. The problem I'm seeing is that the request.getconfiguration().formatters, or odatamediatypeformatters.create both seem to create type specific deserializers.

When I call ReadAsAsync I receive the error: No MediaTypeFormatter is available to read an object of type 'User' from content with media type 'application/atom+xml'.

As the post shows above, the context of the controller is a Tenant, so I believe the formatters are typed to a Tenant and so can't deserialize the User.

I tried manually creating a formatter, _childDefinition is the EDM Type Reference from the navigation property of the odatapath. In this case a User.

Dim dser As New Formatter.Deserialization.ODataEntityDeserializer(_childDefinition, New Formatter.Deserialization.DefaultODataDeserializerProvider)

Dim ser As New Formatter.Serialization.ODataEntityTypeSerializer(_childDefinition, New Formatter.Serialization.DefaultODataSerializerProvider)

Dim dprovider As New Formatter.Deserialization.DefaultODataDeserializerProvider
dprovider.SetEdmTypeDeserializer(_childDefinition, dser)

Dim sprovider As New Formatter.Serialization.DefaultODataSerializerProvider
sprovider.SetEdmTypeSerializer(_childDefinition, ser)

Dim fmt As New Formatter.ODataMediaTypeFormatter(dprovider, sprovider, New List(Of Microsoft.Data.OData.ODataPayloadKind) From {Microsoft.Data.OData.ODataPayloadKind.Entry})
fmt.SupportedEncodings.Add(System.Text.Encoding.UTF8)
fmt.SupportedEncodings.Add(System.Text.Encoding.Unicode)
fmt.SupportedMediaTypes.Add(Headers.MediaTypeHeaderValue.Parse("application/atom+xml;type=entry"))
fmt.SupportedMediaTypes.Add(New Headers.MediaTypeHeaderValue("application/atom+xml"))

I have then tried:

request.content.ReadAsAsync(of User)(new List(of odatamediatypeformatter) from {fmt})
request.content.ReadAsAsync(of User)(request.getConfiguration().formatters)
request.content.ReadAsAsync(of User)(odatamediatypeformatters.create)

All giving the same error, it seems like I'm missing something obvious.

Thanks!

Steve


Solution

  • Just add a parameter of type User to your AddRelation action and you should be good. Web API would automatically invoke the OData formatter to read the User from the request body and bind it to your action parameter.

    You can use this helper method to read OData request body,

    private static Task<T> ReadODataContentAsAsync<T>(HttpRequestMessage request)
    {
        var formatters =
            ODataMediaTypeFormatters.Create()
            .Select(formatter => formatter.GetPerRequestFormatterInstance(typeof(T), request, request.Content.Headers.ContentType));
        return request.Content.ReadAsAsync<T>(formatters);
    }
    

    like this,

    Customer addedCustomer = ReadODataContentAsAsync<Customer>(Request).Result;