8
votes

I've been trying to find a solution for custom serializing an entity returned from an OData controller for the past 2 months! Please help!!!

The use case is pretty simple and I have simplified it even more to get to problematic point. I have some virtual fields attached to some entities in my model, for ex:

public class Customer
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string VirtualField1 { get; set; }
    public string VirtualField2 { get; set; }
    public string VirtualField3 { get; set; }
}

Now, say the client has configured VirtualField1 to be "CompanyName".
All I want to do is create a custom JSON serializer and deserializer that:

  1. Any GET request for a Customer (and of course Customers - that is - IQueryable<>) will go through this serializer which will replace the name of the field "VirtualField1" with "CompanyName" in case of a collection for each Customer.
  2. Any POST request will go through the opposite replacement - that is - replacing the "CompanyName" with "VirtualField1".
    ** The actual replacement logic is a bit more complex but the idea is the same.

I've read everything google could have found, but couldn't found any working example. Here are some links:
https://aspnetwebstack.codeplex.com/wikipage?title=OData%20formatter%20extensibility
** The current OData API is a bit different now, but I figured the principles are the same.
customizing odata output from asp.net web api
Using OData in webapi for properties known only at runtime

The common for all links (and any information I've found for that matter) is I have to inherit from DefaultODataSerializerProvider and add it to my formatters:
On WebApiConfig.cs:

       var customFormatters = ODataMediaTypeFormatters.Create(new CustomODataSerilizerProvider(), new CustomODataDeSerilizerProvider());
       config.Formatters.InsertRange(0, customFormatters);

and the actual provider and serializer:

 public class CustomODataSerilizerProvider : DefaultODataSerializerProvider
 {
    public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.IsEntity())
        {
            return new CustomODataEntityTypeSerializer(edmType.AsEntity(), this);
        }

        return base.GetEdmTypeSerializer(edmType);
    }
 }

** edmType.IsEntity() is never true for IQueryable results so it never creates the concrete serializer. If I force creation it still doesn't break on CreateEntity (or any other create method for that matter).

  public class CustomODataEntityTypeSerializer : ODataEntityTypeSerializer
  {
    public CustomODataEntityTypeSerializer(IEdmEntityTypeReference entityType, ODataSerializerProvider serializerProvider)
        : base(serializerProvider)
    {
    }

    public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
    {
        var oDataEntry = base.CreateEntry(selectExpandNode, entityInstanceContext);
        return oDataEntry;
    }
 }

If I change the concrete serializer to inherit from ODataCollectionSerializer:

  public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {

        if (edmType.IsCollection())
        {
            return new CollectionSerilizer(this);
        }

        return base.GetEdmTypeSerializer(edmType);
    }

and

public class CollectionSerilizer : ODataCollectionSerializer
{
    public CollectionSerilizer(ODataSerializerProvider serializerProvider) : base(serializerProvider)
    {
    }

    public override ODataCollectionValue CreateODataCollectionValue(IEnumerable enumerable, IEdmTypeReference elementType,
        ODataSerializerContext writeContext)
    {
        var oDataCollectionValue = base.CreateODataCollectionValue(enumerable, elementType, writeContext);
        return oDataCollectionValue;
    }

    public override void WriteObject(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
    {
        base.WriteObject(graph, type, messageWriter, writeContext);
    }
}

It does stop on breakpoint on WriteObject but doesn't work and the base is throwing: "The type 'Models.Customer' specified as the collection's item type is not primitive or complex. An ODataCollectionWriter can only write collections of primitive or complex values."

Another funny thing is even if I insert the supposedly default providers:

var customFormatters = ODataMediaTypeFormatters.Create(new DefaultODataSerializerProvider(), new DefaultODataDeserializerProvider());
        config.Formatters.InsertRange(0, customFormatters);

regardless of their position, that is either at the beginning:

config.Formatters.InsertRange(0, customFormatters);

or at the end:

config.Formatters.AddRange(customFormatters);

The OData functionality - that is - for example: $exapnd as in /odata/Customers?$expand=Images completely disappears and doesn't work at all (here is the response):

 [{"Images":[],"CustomerId":1,"FirstName":"Bla","LastName":"Bla", "VirtualField1":null]

the images in this instance are not expanded although without adding the custom formatters they do.

Any thoughts, ideas, directions???

1
Did you ever find an answer to this, I have a type with a custom collection which I use a custom JsonConverter this works ok for vanilla WebAPI, but as soon as I introduce OData to the mix the object returned from the API can no longer be deserialized in my client. I am finding very little on how to get this working. If you found a solution could you share it please?John
Unfortunately, I didn't and as far as I could have figured out it cannot be done using ODate. The reason is deep into the serialization mechanism of OData, which is deeply coupled to the model. Hence modifying the model ruins the serialization. The only possible way I think exists is adding some filter After serialization has occurred. Good luck and please add some info in case you find a way.Tomer
I gave up with this approach. I decided to remove the OData package and stick with vanilla WebAPI as creating and working with Trackable entities was more important for me.John
Good luck! Using API is great, however be careful when sending a cyclic dependent object, since the JSON serializer, doesn't do a great job doing so (A contains B contains A, will basically cause an infinity response if not dealt with, this is obviously spared using OData since you have to use $expand to get an object accessors).Tomer
Thanks. as an aside: WebAPI now uses Json.NET and it is quite good at handling ref loops.John

1 Answers

4
votes

It's probably too late to answer, but I've noticed a significant difference in usability when it comes to serialization between Microsoft's OData toolkit when it comes to OData V1-3 and OData V4.

I think you're using V4, because I too have tried to get the hang of the DefaultODataSerializerProvider, and was unsuccessful.

Then I started a new Web Api project, added all NuGet packages for OData V1-3, added a Web Api OData controller and was immediately successful in any form of serialization customization.

This is because of the simple fact that OData V1-3 works with custom MediaTypeFormatters, just like WebApi. After that it became easy as pie.

I'm not going to insert code about this because examples of using MediaTypeFormatter are plenty, like here: http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters

Ok, it's true that you'll lose some V4 features: http://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-22#OData

But I'm convinced you can live without the hassle of the strict V4 Oasis implementation that Microsoft follows in a holier-than-thou kind of way. (probably because it was their stuff to begin with).

The most important features in V4: Support for aliasing properties in OData model

  • Who cares.

Support for ComplexTypeAttribute, AssociationAttribute, TimesTampAttribute and ConcurrencyCheckAttribute in ODataConventionModelBuilder

  • You don't need them.

Provide ability to supply friendly Title for actions

  • Who cares.

Integrate with ODL UriParser

  • I don't understand.

Support for enum, containment and singleton

  • You don't need that.

Support cast for primitive types

  • You don't need that.

Added OData function support

  • This is unfortunate, but easy solvable by using a ordinary ApiController next to the OData one.

Support parameter aliases for function calls

  • You don't need that, it's messy anyway.

Support camel case naming convention in model

  • Nice that it has support, but I would advice against even doing that.

Support for cast() in $filter

  • Don't need it.

Support for open complex type

  • Yeah and? You have custom serialization. You can serialize any type you want.

Removed EntitySetController and AsyncEntitySetController

  • Good riddance.

Changed $link to $ref

  • Ok.

Added Attribute routing support

  • Unfortunate, so you're stuck with the route you define in the WebApiConfig. Big deal.

Ask yourself, what are you using OData for? I can't speak for your situation obviously, but I'm going to take a crack at what your use case is: - You are trying to allow complex questions to be asked to a data source over http, since you don't want to change the interface every time client N thinks up a new question. - You are trying to allow the client to request what the actual format of the data is he/she gets back. Csv? Xml? Json? vCard? Heck, odt? pdf?

If those two goals are the two you really want to achieve, stick with V3. You'll save yourself a whole bunch of anguish realizing your domain model.

My 0.02$ though.