This post is to guide you through the custom payload format support in ODataLib, which was introduced in ODataLib 6.9.0 release.
All the sample codes in this post have been put to ODataSamples project, you can check it out under Scenarios\CustomFormat folder.
The OData specification defined several payload formats for data exchange. ODataLib has got built-in support for the JSON format(see OData JSON Format spec). But there are cases when someone tries to build up a RESTful service following OData conventions, and also wants to use a custom payload format other than JSON. For example, assuming we have an existing service that uses a custom data format understood by existing clients. If we change the service to OData, we may prefer to keep the current data format, so that while taking advantage of OData features, existing clients could still consume the data returned by the service, or generate request that the service could read.
The OData library was designed to support various payload formats, however, some of those read/write APIs were not publicly visible. In 6.9.0 release, we changed some of those APIs to be public, so that users are able to write out payloads with custom format.
In the following section, we’ll first start by looking at the overall architecture of ODataLib’s reader/writer component and the media type resolving process. Then I will give a demo on how to write a custom payload extension for CSV (comma separated value) format.
Here is a figure to describe the main classes used by ODataLib’s reader and writer component.
[click to enlarge]
As you can see, the ODataLib’s reader and writer share similar structure. The main entry point classes are ODataMessageReader and ODataMessageWriter. They both take a message and a setting class as input. Then when user calls some read/write APIs, ODataMessageReader/ODataMessageWriter would internally figure out the proper payload format to use, then perform corresponding input/output actions using that format.
Let’s take a look at what happens when user tries to write an entry in response payload:
- User prepares an OData response message, sets the header information, and then creates an ODataMessageWriter;
- User calls CreateODataEntryWriter method on ODataMessageWriter, and gets a format specific ODataWriter;
- User calls writing actions on the ODataWriter, such as StartEntry, EndEntry, etc.
In step 2, when creating the format specific ODataWriter, ODataMessageWriter would at first figure out what the payload format to use, we call this media type resolving. After that, an ODataFormat instance is present. Later on the ODataMessageWriter calls the CreateOutputContext method on ODataFormat to get the format specific output context, then calls the corresponding method on the context (CreateEntryWriter in this case).
Media Type Resolving
For media type resolving, the input is the content-type header from ODataMessage, or instructions from settings (for writing only), and the output is an ODataFormat class. The ODataFormat represents the actual payload format, and it is the key point for decoupling various formats with reader/writer APIs. Besides, we have ODataMediaType class which represents a certain kind of media type. Also, we have got ODataMediaTypeFormat that gets one ODataFormat bound to an ODataMediaType. At last, we provide MediaTypeResolver class responsible for choosing correct media types.
MediaTypeResolver class contains following method:
public virtual IEnumerable<ODataMediaTypeFormat> GetMediaTypeFormats(ODataPayloadKind payloadKind)
This API should give out which media types are available for given payload kind. Internally, ODataLib’s reader and writer would first call this method for certain payload kind to get a list of supported ODataMediaTypeFormat, then it would choose the best match based on media type information from request message.
In general, the default implementation of MediaTypeResolver would return JSON format for data request, and XML format for metadata request. Derived classes could choose to override this method to have their own behavior. Thus users could provide a custom media payload format by overriding it and returning the expected ODataMeidaFormat. We’ll demo how to write a custom MediaTypeResolver in following section.
Implementing a CSV Format extension
Here we’ll have a demo on how to implement an extension that supports writing out CSV format payload. Our goal is to support writing an ODataEntry as a CSV format, every property appear to be in a single column. Please make sure your project have installed Microsoft.OData.Core 6.9.0 package from NuGet gallery before getting started.
At first, we will implement the class CsvWriter and CsvOutputContext.
- The class CsvWriter derives from ODataWriter, to simplify the demo, we omitted some validation logic and the async API implementation here.
- The class CsvOutputContext derived from ODataOutputContext, this class acts as a bridge between ODataMessageWriter and specific writer, we'll return CsvWriter instances from CreateODataEntryWriter/CreateODataFeedWriter method here.
Then we can write our own CsvFormat class, which is quite simple. Please note that we omit implementation for other abstract method here.
At last, we can implement the MediaTypeResolver for CSV, here we’ll bind media type ‘text/csv’ to our CsvFormat.
Here is a usage demo for writing an OData feed with CSV format. Please note that we almost use same code logic for writing JSON payload here. The only difference is we’ll set our CsvMediaTypeResolver to the MessageWriterSettings, while the message’s content type header is set to CSV format.
Here is the output:
In our ODataSample project, we have a WebAPI service project which supports both CSV and VCard formats, you can check it out here.