Media Resource Support for OData in Web API

OData has long had solid support for streams via Media Link Entries (MLE) and Media Resources (MR), but the things you need to do in order to enable them in Microsoft ASP.NET Web API is significantly different from the mechanics used in WCF Data Services. Initially I wasn't even sure whether there was any support for OData streaming support in Web API, but a short discussion with the OData Web API team confirmed that all the plumbing is in place; however, there aren't any controllers or other out-of-the-box base classes that support it - yet.

Fortunately, the team was kind enough to provide an example of the world's simplest implementation of a bare-bones controller with support for Media Resources. With a naïve, but functional example in-hand, I've been able to take their example and refactor the bits into something that can be reused by all. In this post, I'll illustrate the pieces that you need to configure in order to enable OData streaming and I'll discuss the MediaResourceController base class that will allow you to quickly enable Media Resources in your application.

Required Components

In order to enable streaming in accordance with the OData specification, there's a couple of things we must create:

  • A custom OData routing convention
  • A custom OData path handler
  • A custom OData entity type serializer
  • An OData MediaResourceController

That might seem like a lot to create, but most of the work relies on just extending the base classes already available in the OData assembly for Web API. I'll leave a deep analysis of the code to the reader, but I'll highlight the important segments in the sections to follow.

Media Resource Entry Routing Convention

The first thing we need is a way to select a controller to dispatch Media Resource related links to. Since the mapping and semantics of OData are more well-defined than a basic Web API controller, we can't simply decorate a method such as GetMediaResource with the HttpGetAttribute and expect it to light up. However, since the operations for a Media Resource will intrinsically be CRUD operations for the resource and queries would never be supported for a stream, it is relatively straightforward to define a generic routing convention.

We begin by inheriting from the EntityRoutingConvention. Our routing convention only needs to override the SelectAction method. We'll be looking to see if we get an OData path template that matches ~/entityset/key/$value. If we get a match, then we'll use the Convention-Over-Configuration semantics commonly used in ASP.NET MVC and Web API to match the HTTP method to one of four expected actions: GetMediaResource, PostMediaResource, PutMediaResource, or DeleteMediaResource.

Media Resource Entry Path Handler

Creating a custom path handler to process the Media Resource OData segment is one of the easiest tasks. We start by inheriting from the DefaultODataPathHandler and override the ParseAtEntity method. The only thing we need to do is return the ValuePathSegment if the segment equals $value; otherwise, we just let the base implementation work its magic.

Media Link Entry Entity Type Serializer

The secret to rendering the Media Link Entry in OData responses is to override the CreateEntry method in the ODataEntityTypeSerializer. In this method, we need to detect whether the entity being serialized has a Media Resource (e.g. stream) associated with its model. If it does, then we need to add a Media Link Entry for the Media Resource. If there is no associated Media Resource defined, then we just let the base implementation run its course.

The one challenge we face with this solution is that the way the Media Link Entry is created can vary by controller and entity. Hard-coding or otherwise having to create a new serializer for each variant simply will not do. To solve this problem, as illustrated in the above example, we'll define an IMediaLinkEntryProvider interface and connect it to the current request using the extension methods GetMediaLinkEntryProvider and SetMediaLinkEntryProvider. The interface itself only contains one method - GetMediaLinkEntry, which provides a callback mechanism for a controller to decide how or if a Media Link Entry should be created. A controller might be read-only or the current entity might have a Media Resource, but it is currently unset. There is no clear way for the serializer to be cognizant of this information.

Media Resource Controller

We now have all the constituent components required to create a reusable Media Resource controller. We start by extending the AsyncEntitySetController. The MediaResourceController has the following, basic signature:

There are three things you must do in addition to what is normally required to set up an AsyncEntityController.

  • Override the GetStream method to retrieve a stream from an entity. The way the stream is retrieved can vary as your entity may have an associated Stream, a byte array, or may use some other mechanism to retrieve the stream.
  • Override the GetContentTypeForStream method to retrieve the stream media type. The media type cannot be derived directly from the Stream; however, your entity should have an attribute that can provide it.
  • Override the GetMediaLinkEntry method to create a Media Link Entry for an entity. This will provide the implementation for the IMediaEntryLinkProvider interface.

As is the case with the AsyncEntityController, the default implementation of the PostMediaResource, PutMediaResource, and DeleteMediaResource will return HTTP 501 (Not Implemented). By overriding the Initialize method, the MediaResourceController will assign itself as the IMediaEntryLinkProvider associated with the current request; therefore, there is nothing for you to do to set this up in your controllers.

Batching and Partial Content

Depending on the size of your Media Resources, it may be desirable to support batching and partial content. Fortunately, this is really easy to support in ASP.NET Web API using the Range HTTP header and the ByteRangeStreamContent class. The partial stream behavior is baked into the default implementation of the GetMediaResource method, so there is nothing extra for you to do in order to take advantage of this capability.

Creating Your First Media Resource Controller

To demonstrate all the pieces coming together, we'll create a simple, read-only Media Resource controller for an entity set of images. The first thing we need is our entity definition for an Image.

Creating a controller with our defined entity is now a cinch. In the spirit of keeping the demonstration simple, we'll just enumerate the images in a folder hosted on the website. In this example, we also build up the entity and its corresponding stream in a single pass. To prevent unnecessarily opening a FileStream, we'll use the provided LazyStream, which enables a lazy-loaded stream. As noted previously, this technique is only one of several possibilities for returning the Stream in the GetStream method.

The majority of the controller's implementation shouldn't require much explanation. The GetMediaLinkEntry is the only part that isn't entirely straightforward. To ensure developers have the maximum fidelity over how the Media Link Entry is created, you are provided the entity and the serializer context being used. Given this information, we are able to create a Media Link Entry with a read link and its media type. We could optionally add an edit link and ETag. The edit link would typically be the same as the read link, but will be routed based on the POST, PUT, or DELETE verb.

Setting Up the Web API Configuration

The final step that must be completed before we can reap the rewards of our efforts is to update the Web API configuration. First, we need an EDM model to provide to the OData route.

The last thing we need to do is update the Web API registration with the required OData configuration. We override the default OData routing conventions, path handler, and serializer provider with our extended implementations that support Media Link Entries.

Viewing the Results

At long last we can hit F5 and view the results. A single entity result in JSON now looks like:

If we following the Media Link Entry, we'll get the associated Media Resource as an image:

Conclusion

Ultimately this post ended up being longer than I wanted; however, I felt it was important to outline all of the moving parts required to enable Media Resources and Media Link Entries. I didn't demonstrate a controller that writes Media Resources, but it's not much of a stretch to implement one by overriding the appropriate methods and reading the stream sent in a request. Hopefully you find that using the sample library provided, it is relatively quick and easy to setup any number of Media Resource controllers in your application.

Although omitted from the post for brevity, all of the code provided is well-documented to help guide you through it. Of course, no example would be complete without unit tests, so all of the related unit tests are also included with the source. I'm a big fan of XUnit.NET, so if you don't have the test runner installed, you'll need to grab it from the Visual Studio Extension Gallery. I'm also a fan of Design-By-Contract and Microsoft Code Contracts. The code contracts defined are unobtrusive, require no special tools, and can even be removed if you so desire; however, if you want to leverage the full Code Contracts toolset, you can also get it from the Visual Studio Extension Gallery. The solution was built with Visual Studio .NET 2013, .NET 4.5.1, ASP.NET MVC 5.0, and ASP.NET Web API 2.0 for OData.

ODataMediaLinkEntries.zip