OData formatter extensibility

Extensibility points

1) Create formatters.
ODataMediaTypeFormatters.Create(ODataSerializerProvider serializerProvider, ODataDeserializerProvider deserializerProvider)

Entity serialization extensibility

2) Modify ODataEntry before writing it - override this method.
public virtual ODataEntry CreateEntry(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext);

3) Control creation of navigation links - override this method.
public virtual IEnumerable<ODataNavigationLink> CreateNavigationLinks(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext);

4) Control creation of odata properties - override this method (example named streams).
public virtual IEnumerable<ODataProperty> CreateStructuralPropertyBag(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext);
public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, object entityInstance, ODataSerializerContext writeContext;

5) Control creation of OData actions - override this method.
public virtual IEnumerable<ODataAction> CreateODataActions(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext);
public virtual ODataAction CreateODataAction(IEdmFunctionImport action, EntityInstanceContext entityInstanceContext, ODataMetadataLevel metadataLevel);

Feed serialization extensibility

6) Control the ODataFeed object being written - override this method.
public virtual ODataFeed CreateODataFeed(IEnumerable feedInstance, ODataSerializerContext writeContext);

Usage scenarios

Creating formatters

Serializer and deserializer providers are public and extensible. ODataMediaTypeFormatters has a method that takes custom serializer and deserializer providers.
ODataMediaTypeFormatters.Create(new MyODataSerializerProvider(), new DefaultODataDeserializerProvider());

Plugging in custom formatters

There are two scenarios here.

Scenario 1 - Global formatters

var customFormatters = ODataMediaTypeFormatters.Create(new MyODataSerializerProvider(), new DefaultODataDeserializerProvider());
config.Formatters.AddRange(customFormatters);

Scenario 2 - ODataController

Per-controller configuration makes it harder to modify formatters. That said, OData formatters can be registered globally and will not take over the whole application.

public class CustomODataFormattingAttribute : ODataFormattingAttribute
{
    public override IList<ODataMediaTypeFormatter> CreateODataFormatters()
    {
        IList<ODataMediaTypeFormatter> formatters = ODataMediaTypeFormatters.Create(new MyODataSerializerProvider(), new DefaultODataDeserializerProvider());
        return formatters;
    }
}

[CustomODataFormatting]
[ODataRouting]
public class CustomODataController : ApiController
{
}

Plugging in custom serializer

You have to implement a custom serializer provider.

public class MyODataSerializerProvider : DefaultODataSerializerProvider
{
    public override ODataSerializer CreateEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.IsEntity())
        {
            return new CustomODataEntityTypeSerializer(edmType.AsEntity(), this);
        }

        return base.CreateEdmTypeSerializer(edmType);
    }
}

Plugging in custom deserializer is similar.

Supporting Atom entry metadata

You implement a custom ODataEntityTypeSerializer and plug it in.
public class CustomODataEntityTypeSerializer : ODataEntityTypeSerializer
{
    public CustomODataEntityTypeSerializer(IEdmEntityTypeReference entityType, ODataSerializerProvider serializerProvider)
        : base(entityType, serializerProvider)
    {
    }

    public override ODataEntry CreateEntry(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext)
    {
        ODataEntry entry = base.CreateEntry(entityInstanceContext, writeContext);
        
        // Set the ATOM title metadata
        entry.Atom().Title = new AtomTextConstruct { Text = "Custom title" };

        return entry;
    }
}

Supporting named streams

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

    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, object entityInstance, ODataSerializerContext writeContext)
    {
        if (structuralProperty.Type.IsStream())
        {
            return new ODataProperty { Name = structuralProperty.Name, Value = new ODataStreamReferenceValue { ReadLink = new Uri("http://image") } };
        }

        return base.CreateStructuralProperty(structuralProperty, entityInstance, writeContext);
    }
}

Supporting Media link entries

update model.
private static IEdmModel GetEdmModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Customer>("customers");
    IEdmModel model = builder.GetEdmModel();
    model.SetHasDefaultStream(model.FindDeclaredType("ODataTests.Customer") as IEdmEntityType, hasStream: true);
    return model;
}

custom entry serializer.
public class CustomODataEntityTypeSerializer : ODataEntityTypeSerializer
{
    public CustomODataEntityTypeSerializer(IEdmEntityTypeReference entityType, ODataSerializerProvider serializerProvider)
        : base(entityType, serializerProvider)
    {
    }

    public override ODataEntry CreateEntry(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext)
    {
        ODataEntry entry = base.CreateEntry(entityInstanceContext, writeContext);
        entry.MediaResource = new ODataStreamReferenceValue
        {
            ReadLink = new Uri("http://myazurecdn/"),
            ContentType = "application/png"
        };

        return entry;
    }
}

Resultant json.
1.PNG

Supporting instance annotations

As an example, lets say we want to put previous page link as well on feeds using instance annotations on feeds.
public class CustomODataFeedSerializer : ODataFeedSerializer
{
    public CustomODataFeedSerializer(IEdmCollectionTypeReference edmCollectionType, ODataSerializerProvider serializerProvider)
        : base(edmCollectionType, serializerProvider)
    {
    }

    public override ODataFeed CreateODataFeed(IEnumerable feedInstance, ODataSerializerContext writeContext)
    {
        ODataFeed feed = base.CreateODataFeed(feedInstance, writeContext);
        feed.InstanceAnnotations.Add(new ODataInstanceAnnotation("ns.previous", new ODataPrimitiveValue("http://previouspagelink")));
        return feed;
    }
}

and the on wire annotation looks like,
prev-page-link.PNG

Client side formatter

one of the goals of this exercise is to open up the serializer/de-serializers enough so that one can write a client-side formatter. I will add a sample here soon.

Last edited Apr 10, 2013 at 5:39 AM by raghuramn, version 3

Comments

rcollette Aug 6, 2014 at 10:14 PM 
I am trying to implement instance annotations on a ODataFeed as shown in the example here but the annotations are never inluded in the output. I include the header Prefer: odata.include-annotations="*" but no annotations are included. Looking through source, the internal property Microsoft.OData.Core.MessageWriterSettings.ShouldInludeAnnotations is never set to a function and therefore, the ODataJsonLightValueSerializer.ShouldSkipAnnotation() method is called, it always returns true.

Neilio999 Jul 2, 2014 at 1:17 PM 
I tried to write a CSV ODataMediaTypeFormatter serializer. After writing classes down to FeedSerializer, there are 2 functions blocking my way. In FeedSerializer.WriteObject it called writeContext.GetEdmType, which is internal. I got around this by creating an extension method that used reflection to call it. But the final blockage is messageWriter.CreateODataFeedWriter, which looks up the content type in an internal list, which cannot be modified without rebulding the odata library.

Is there any movement on opening up formatters?

BasHamer May 8, 2014 at 5:08 PM 
some version info would be great, but it looks like

public virtual IEnumerable<ODataProperty> CreateStructuralPropertyBag(EntityInstanceContext entityInstanceContext, ODataSerializerContext writeContext);

is not supported any more in Assembly System.Web.Http.OData.dll, v5.1.0.0; and it looks like I'm up to date on the package (Wednesday, April 02 2014).

not sure where to find this functionality so it respects IDynamicMetaObjectProvider

wiseshell Jan 28, 2014 at 3:39 PM 
Can you PPPLLLEEEAAASSSEEEE provide a functional example with the current API (that was slightly modified)?

dkillewo Oct 10, 2013 at 6:08 PM 
Are there OData formatter that would enable JSONP support?