Freigeben über


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

Comments

  • Anonymous
    November 11, 2013
    Thanks for a great post! However i fail to open the zip archive?

  • Anonymous
    November 12, 2013
    It looks like the file was corrupt. I reattached the archive.  It appears to be working now.  Let me know if you continue to have any issues with it.  Thanks.

  • Anonymous
    December 31, 2013
    Good Post !!!

  • Anonymous
    April 15, 2014
    Excellent post thanks. Really great to find material on supporting MediaResources under WebApi. The OData libraries are changing fast and this post is now 5 months old. Is this code now baked into the libraries or is this still the way to go?

  • Anonymous
    April 16, 2014
    Wow! I can't believe it's been that long already.  As far as I can tell, there still isn't anything directly baked into the libraries yet to make working with Media Resources any easier.  OData functions still aren't supported at all.  I'm not sure exactly what the team is working on, but they may be focusing on that first because, although there is some tedious setup work required to support Media Link Entries, it is at least possible with the current libraries.  There are some rough examples documented at: aspnetwebstack.codeplex.com/wikipage. If I get some extra time, I'll see about reaching out to the team.  Perhaps I can have them review this code and roll it in as a contribution.  Until then, I think this is the only way to go.  I hope that helps.

  • Anonymous
    April 20, 2014
    The comment has been removed

  • Anonymous
    May 03, 2014
    Excellent Post Chris thanks for Sharing your experience. I wanted to extend this to support POST, PATCH, DELETE and I'm having trouble setting up. Have you manages to implement POST and/or PATCH? If so can you please share with us. Thinks in advance , Pavan

  • Anonymous
    May 05, 2014
    @AndyKiwi: I can't honestly say that don't I have a deep understanding of the idiosyncrasies of the WCF Data Services client.  Streaming and Media Resources support always felt kludgy to me in Data Services.  I'm not sure exactly what your sending or the sequence that your sending it in, but creating a new Media Resource should be POSTed to a URL that matches the template "~/entityset/key/$value".  As the template suggests, you have to have an entity before you can attach a Media Resource to it, which makes sense IMO.  If you wanted to do both in a single client request, you could easily enable batch support and craft the request as a batch with a POST for the entity, followed by a POST for the Media Resource. With respect to named streams, I haven't had an immediate need for them so I haven't given it much thought.  It's definitely worth having though.  When I can spare so time (or the need becomes necessary), I'll look at refactoring to support them.  When I come up with something, I'll post a follow up article.

  • Anonymous
    May 05, 2014
    @Pavan Kumar: POST, PUT, and DELETE should be already be supported.  As long as the request matches the template "~/entityset/key/$value", it should map to the appropriate CreateMediaResource, UpdateMediaResource, or DeleteMediaResource.  You just need to override the appropriate method and handle the stream.  You can retrieve the stream from the incoming Request property. In hindsight, I probably should have mapped a Stream parameter using the [FromBody] attribute for model binding.  You have the source and can change it if you like. I didn't provide support for PATCH or MERGE, but you could easily do so by refactoring the routing convention and MediaResourceController. The reason I didn't support it is because updating part of a stream is not a common scenario.  Typically, it's all or nothing.  In fact, POST and PUT will most likely have the same implementation logic.  The only feasible scenario I can think of for allowing a partial update is uploading in chunks, but I don't know if you'd want to do it that way.  If not all of the parts are received by the server, you could end up with a corrupt Media Resource.  If you can live that, then adding the support for it isn't difficult.

  • Anonymous
    January 13, 2015
    I add a console application in you sample solution and tried to add proxy using OData Client Code Generator, but failed, cs file is empty. Anybody tried to do the same with me please?

  • Anonymous
    January 13, 2015
    Sorry, forget what I posted it, it is because OData version used by Host is not V4.

  • Anonymous
    April 17, 2015
    So how does one go about setting this up using OData v4 instead of v3?

  • Anonymous
    May 05, 2015
    The comment has been removed

  • Anonymous
    May 20, 2015
    I created a sample OData v4 version of this project. See: github.com/.../ODataFileServer

  • Anonymous
    December 01, 2015
    @Vojtech your sample misses things like link construction as in this example

  • Anonymous
    December 02, 2015
    Further research, this post is completely irrelevant. You don't need to add custom router nor path handler .   [ODataRoute("(items{key)/$value")] will simply do. The only part that's worthy is how you add the stream link to the result. We use MediaType() call and write code similar to            var key = ODataUriUtils.ConvertToUriLiteral(entityKey, ODataVersion.V4);            var keyValuePath = new KeyValuePathSegment(key);            var val = new ValuePathSegment();            var es = new EntitySetPathSegment((IEdmEntitySetBase)entity.NavigationSource);                      var url = new UrlHelper(context.Request);           var link =  url.CreateODataLink(es, keyValuePath, val);            //var routeParams = new { package.Id, package.Version };            return new ODataStreamReferenceValue            {                ContentType = mediaType,                ETag = eTag,                ReadLink = new Uri(link)            };