Today we are going to build a custom formatter for ASP.NET WebAPI, deriving from MediaTypeFormatter class. It will return our model (or collection of models) in RSS or Atom format.
The Formatter is supposed to react to requests sent with request headers “Accept: application/atom+xml” and “Accept: application/rss+xml”.
Let’s get going.
Update - September 2012
This post was originally written against Web API beta. Since then the RC and then RTM versions have been released. Below is the code updated for the RTM version (the “proper” Web API released on 15th August).
public class SyndicationFeedFormatter : MediaTypeFormatter
{
private readonly string atom = "application/atom+xml";
private readonly string rss = "application/rss+xml";
public SyndicationFeedFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(atom));
SupportedMediaTypes.Add(new MediaTypeHeaderValue(rss));
}
Func<Type, bool> SupportedType = (type) =>
{
if (type == typeof(Url) || type == typeof(IEnumerable<Url>))
return true;
else
return false;
};
public override bool CanReadType(Type type)
{
return SupportedType(type);
}
public override bool CanWriteType(Type type)
{
return SupportedType(type);
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext)
{
return Task.Factory.StartNew(() =>
{
if (type == typeof(Url) || type == typeof(IEnumerable<Url>))
BuildSyndicationFeed(value, writeStream, content.Headers.ContentType.MediaType);
});
}
private void BuildSyndicationFeed(object models, Stream stream, string contenttype)
{
List<SyndicationItem> items = new List<SyndicationItem>();
var feed = new SyndicationFeed()
{
Title = new TextSyndicationContent("My Feed")
};
if (models is IEnumerable<Url>)
{
var enumerator = ((IEnumerable<Url>)models).GetEnumerator();
while (enumerator.MoveNext())
{
items.Add(BuildSyndicationItem(enumerator.Current));
}
}
else
{
items.Add(BuildSyndicationItem((Url)models));
}
feed.Items = items;
using (XmlWriter writer = XmlWriter.Create(stream))
{
if (string.Equals(contenttype, atom))
{
Atom10FeedFormatter atomformatter = new Atom10FeedFormatter(feed);
atomformatter.WriteTo(writer);
}
else
{
Rss20FeedFormatter rssformatter = new Rss20FeedFormatter(feed);
rssformatter.WriteTo(writer);
}
}
}
private SyndicationItem BuildSyndicationItem(Url u)
{
var item = new SyndicationItem()
{
Title = new TextSyndicationContent(u.Title),
BaseUri = new Uri(u.Address),
LastUpdatedTime = u.CreatedAt,
Content = new TextSyndicationContent(u.Description)
};
item.Authors.Add(new SyndicationPerson() { Name = u.CreatedBy });
return item;
}
}
Starting off π
We will start as always when dealing with WebAPI - by creating a new ASP.NET MVC 4 project, and choosing WebAPI template. Make sure you are using Visual Studio 11 beta, cause the source files included in this post are VS11.
Model π
Our model is looking like this:
public class Url
{
public int UrlId { get; set; }
public string Address { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; }
}
For simplicity, we will skip the DB altogether in this exercise, and use the usual repository pattern. The interface is as follows:
public interface IUrlRepository
{
IQueryable<Url> GetAll();
Url Get(int id);
Url Add(Url url);
}
And the repository class as follows. Note that in the constructor we instantiate two objects into the collection so that we have some data to work with. Also, we omitted the Remove() and Update() methods, as they are irrelevant for our today’s examples.
public class UrlRepository : IUrlRepository
{
private List<Url> _urls = new List<Url>();
private int _nextId = 1;
public UrlRepository()
{
this.Add(new Url()
{
UrlId = 1,
Address = "/2012/03/build-facebook-style-infinite-scroll-with-knockout-js-and-last-fm-api/",
Title = "Build Facebook style infinite scroll with knockout.js and Last.fm API",
CreatedBy = "Filip",
CreatedAt = new DateTime(2012, 3, 20),
Description = "Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records β just like the infinite scroll used on Facebook or on Twitter."
});
this.Add(new Url()
{
UrlId = 2,
Address = "/2012/04/your-own-sports-news-site-with-espn-api-and-knockout-js/",
Title = "Your own sports news site with ESPN API and Knockout.js",
CreatedBy = "Filip",
CreatedAt = new DateTime(2012, 4, 8),
Description = "You will be able to browse the latest news from ESPN from all sports categories, as well as filter them by tags. The UI will be powered by KnockoutJS and Twitter bootstrap, and yes, will be a single page. We have already done two projects together using knockout.js β last.fm API infinite scroll and ASP.NET WebAPI file upload. Hopefully we will continue our knockout.js adventures in an exciting, and interesting for you, way."
});
}
public IQueryable<Url> GetAll()
{
return _urls.AsQueryable();
}
public Url Get(int id)
{
return _urls.Find(i => i.UrlId == id);
}
public Url Add(Url url)
{
url.UrlId = _nextId++;
_urls.Add(url);
return url;
}
}
ApiController π
Our ApiController also isn’t going to be anything sophisticated. In fact, for the purposes of this exercise, we are only interested in Get() and Get(int id) methods.
public class ValuesController : ApiController
{
private static readonly IUrlRepository _repo = new UrlRepository();
// GET /api/values
public IEnumerable<Url> Get()
{
return _repo.GetAll();
}
// GET /api/values/5
public Url Get(int id)
{
return _repo.Get(id);
}
}
Basically all the ApiController does is calls the UrlRepository methods to retrieve the specific object instances. Notice that the controller itself doesn’t worry about any serialization or formatting (instead, just returns normal models). This - our core activity for today - is handled elsewhere.
Setting up the MediaTypeFormatter π
Now that we have all the building blocks of the sample web application in place, we can start with our MediaTypeFormatter. I will be deriving from the MediaTypeFormatter class, but you might as well derive your formatter from BufferedMediaTypeFormatter. The difference is that MediaTypeFormatter wraps the read/write methods in asynchrounous methods, while BufferedMediaTypeFormatter uses synchronous ones.
Let’s add a class SyndicationFeedFormatter.cs to our project, and inherit from MediaTypeFormatter.
public class SyndicationFeedFormatter : MediaTypeFormatter
{
}
We will be playing around with application/atom+xml and application/rss+xml MIME types, so we might as well put them in some reusable properties.
private readonly string atom = "application/atom+xml";
private readonly string rss = "application/rss+xml";
In the constructor, we need to tell the formatter that these are the MIME types it should look for in the headers of the request.
public SyndicationFeedFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(atom));
SupportedMediaTypes.Add(new MediaTypeHeaderValue(rss));
}
Next step is to tell the formatter which custom types (models) to process. To do that, we need to override CanWriteType() method of MediaTypeFormatter. In our case, we will support Url type and IEnumerable of Url. If you wish to support all types, you can always simply return true from this method.
protected override bool CanWriteType(Type type)
{
if (type == typeof(Url) || type == typeof(IEnumerable<Url>))
return true;
else
return false;
}
One more override we need to include is OnWriteToStreamAsync(), in which we will tell the formatter how to write the response to the client. This is where we will build up the actual RSS or Atom structure out of our models.
protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)
{
return Task.Factory.StartNew(() =>
{
if (type == typeof(Url) || type == typeof(IEnumerable<Url>))
BuildSyndicationFeed(value, stream, contentHeaders.ContentType.MediaType);
});
}
In this method, we check if the type is one of our supported types, and we call the BuildSyndicationFeed() private method, which will do all the magic.
private void BuildSyndicationFeed(object models, Stream stream, string contenttype)
{
List<SyndicationItem> items = new List<SyndicationItem>();
var feed = new SyndicationFeed()
{
Title = new TextSyndicationContent("My Feed")
};
if (models is IEnumerable<Url>)
{
var enumerator = ((IEnumerable<Url>)models).GetEnumerator();
while (enumerator.MoveNext())
{
items.Add(BuildSyndicationItem(enumerator.Current));
}
}
else
{
items.Add(BuildSyndicationItem((Url)models));
}
feed.Items = items;
using (XmlWriter writer = XmlWriter.Create(stream))
{
if (string.Equals(contenttype, atom)) {
Atom10FeedFormatter atomformatter = new Atom10FeedFormatter(feed);
atomformatter.WriteTo(writer);
} else {
Rss20FeedFormatter rssformatter = new Rss20FeedFormatter(feed);
rssformatter.WriteTo(writer);
}
}
}
This is what happens in the method:
- We create an instance of SyndicationFeed. This is going to be then returned as either Atom- or RSS-structured XML, depending on what the client originally requested via the Headers.
- We check if the object passed from the OnWriteToStreamAsync() is either Url type or IEnumerable of Url. Depedning on that, we’d have to treat it differently.
2a. If it’s IEnumerable, we iterate through the IEnumerable and call BuildSyndicationItem() for each Url instance.
2b. If it’s just a single Url object, then we call that method just once. - BuildSyndicationItem(), into which we’ll look in a moment, returns a SyndicationItem object, which we add to the Items property of the SyndicationFeed instance created beforehand.
- Using the XmlWriter, we write either RSS or Atom feed (depending on the Content Type requested by the client) to the Stream that is going to be flushed as the Response.
The last thing to look at is the BuildSyndiactionItem() method, which transforms our model (Url) into SyndicationItem object.
private SyndicationItem BuildSyndicationItem(Url u)
{
var item = new SyndicationItem()
{
Title = new TextSyndicationContent(u.Title),
BaseUri = new Uri(u.Address),
LastUpdatedTime = u.CreatedAt,
Content = new TextSyndicationContent(u.Description)
};
item.Authors.Add(new SyndicationPerson() { Name = u.CreatedBy });
return item;
}
Hooking up the formatter to GlobalConfiguration π
Finally, we need to add our formatter to GlobalConfiguration in the Global.asax. GlobalConfiguration.Configuration exposes a Formatters collection, which contains all formatters. To include ours there, we add the following line in the Application_Start():
GlobalConfiguration.Configuration.Formatters.Add(new SyndicationFeedFormatter());
Trying it out π
In order to try this out, we’ll add a couple of jQuery $.ajax calls to the Index.cshtml. First let’s try the Atom feed for all items:
$.ajax({
beforeSend: function (request) {
request.setRequestHeader("Accept", "application/atom+xml");
},
url: "http://localhost:56661/api/values"
});
This produces the following output:
Now, let’s try RSS for all items:
$.ajax({
beforeSend: function (request) {
request.setRequestHeader("Accept", "application/rss+xml");
},
url: "http://localhost:56661/api/values"
});
What about Atom and RSS for single item?
First, Atom:
$.ajax({
beforeSend: function (request) {
request.setRequestHeader("Accept", "application/atom+xml");
},
url: "http://localhost:56661/api/values/1"
});
And RSS:
$.ajax({
beforeSend: function (request) {
request.setRequestHeader("Accept", "application/rss+xml");
},
url: "http://localhost:56661/api/values/1"
});
Summary and source code π
As you see it’s quite easy to build your own media type formatters for your ApiControllers. In fact, Gunnar Peipman has written a terrific blog post on extending content negotiation in WebApi here.
In our case, we have created both RSS and Atom formats using one formatter, supporting both a single model and an IEnumerable of model. The only downside of this solution, is that the client needs to specifically ask for MIME application/atom+xml and application/rss+xml. Therefore it’s suitable for RSS/Atom calls made programmatically somewhere from the backend, but not for accessing the feed via the browser, or using the client that doesn’t pass the Content Type headers correctly. In the next post, I’ll show how to work around it, and use this custom formatter also in regular GET calls triggered from the browser or, for that matter, anywhere else.
In the meantime, as always:
- source code (VS11 beta project), ZIP, 6.5MB