Introduction
In this article we will see in brief about formatters in WebAPI. Also we will
see how we can implement custom formatters. Formatters plays a very important
role in WebAPI. At high level , formatters are responsible for returning the
content , i.e the HTTP response based on the content type.
If you recall the concepts of WebAPI Content Negotiation, you will see the
heart of it is the formatters. The formatters are built to read the HTTP Request
and return the HTTP Responses based on the media type. Thus formatters unifies
all the serialization concerns by providing a single entry point for serializing
or deserializing a model using a media type format.
When the content negotiation happens between the client and the server, two
headers are typically involved in this negotiation. The Accept request
header, which specifies the desired response format and the other is the
Content-Type request header, tells how the sever can parse the contents of the
message body . The content negociation implementation has algorithm to
determine which Formatter or Formatters to use at runtime and deserialize and
serialize the body payload on the request and response messages.
Note: There is a default formatter implemented in WebAPI. It provides support
to return all the content in JSON format. That means say if you are not
specifying the accept type header, you should still be able to receive the
default JSON output.
All formatters, either custom or built-in derives from the base class
MediaTypeFormatter. The formatter after deriving can override CanReadType and
ReadFromStreamAsync method for supporting deserialization and CanWriteType and
WriteToStreamAsync method for supporting serialization of models into a given
media type format.
In order to create a custom formatter one can either extend
MediaTypeFormatter or BufferedMediaTypeFormatter and override read and write
methods.
MediaTypeFormatter - This class uses asynchronous read and
write methods.
BufferedMediaTypeFormatter - This class
derives from MediaTypeFormatter but wraps the asynchronous read/write methods
inside synchronous methods. Deriving from BufferedMediaTypeFormatter is simpler,
because there is no asynchronous code, but it also means the calling thread can
block during I/O.
Below is the picture, shows at high level how the custom formatters are used to format the HTTP Response before sending them to client.
Background
Understanding of ASP.NET WebAPI. Please refer my beginners article on WebAPI.
It is very important to just glance at my WebAPI article http://www.codeproject.com/Articles/549152/Introduction-to-ASP-NET-Web-API
as we are reusing the ProductsLibrary WebAPI class library project that was
built in the introductory WebAPI article.
Using the code
Code Snippet for MediaTypeFormatter
public abstract class MediaTypeFormatter
{
public Collection<encoding> SupportedEncodings { get; }
public Collection<mediatypeheadervalue> SupportedMediaTypes { get; }
public Collection<mediatypemapping> MediaTypeMappings { get; }
public abstract bool CanReadType(Type type);
public abstract bool CanWriteType(Type type);
public virtual Task<cke:object> ReadFromStreamAsync(Type type, Stream readStream,
HttpContent content, IFormatterLogger formatterLogger);
public virtual Task WriteToStreamAsync(Type type, object value,
Stream writeStream, HttpContent content, TransportContext transportContext);
}
It's an abstract class. To implement custom formatters one has to extend
and implement methods for serialize and deserialize the response and
requests. The CanReadType indicates whether the formatter can read from Stream i.e deserialize the request body content.
If you wish to support this , you will have to implement the ReadFromStreamAsync
method and similarly the CanWriteType and WriteToStreamAsync method for
serializing the response.
Implementing Custom Formatters:
Below is the code snippet for UriBasedRssAtomMediaTypeFormatter.
The CanReadType and CanWriteType checks whether the type is URI and proceeds
to format the response if it's an URI. The important part of the formatter is
to provide support for reading and writing content based on the content type of
the request.
The below formatter - UriBasedRssAtomMediaTypeFormatter derives from the base
MediaTypeFormatter class and override the WriteToStreamAsync so that provides
outputing RSS or Atom responses.
Note: We are using Atom10FeedFormatter and Rss20FeedFormatter to format the
Http reponse into feeds.
public class UriBasedRssAtomMediaTypeFormatter : MediaTypeFormatter
{
private readonly string atom = "application/atom+xml";
private readonly string rss = "application/rss+xml";
public UriBasedRssAtomMediaTypeFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(atom));
SupportedMediaTypes.Add(new MediaTypeHeaderValue(rss));
}
public override bool CanReadType(Type type)
{
return type == typeof(Uri);
}
public override bool CanWriteType(Type type)
{
return type == typeof(Uri);
}
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(Uri))
BuildSyndicationFeed(value, writeStream, content.Headers.ContentType.MediaType);
});
}
private void BuildSyndicationFeed(object uri, Stream stream, string contenttype)
{
var url = (uri as Uri).OriginalString;
SyndicationFeedFormatter formatter;
XmlTextReader reader = new XmlTextReader(url);
SyndicationFeed feed = SyndicationFeed.Load(reader);
if (contenttype == "application/atom+xml")
{
formatter = new Atom10FeedFormatter(feed);
}
else
{
formatter = new Rss20FeedFormatter(feed);
}
using (var writer = XmlWriter.Create(stream))
{
formatter.WriteTo(writer);
writer.Flush();
writer.Close();
}
}
}
Below is the code snippet for CustomSyndicationFeedFormatter.
We are using this formatter for building the Products feeds, a response from
the ProductsApiController. After extending from MediaTypeFormatter , we will
have to implement the methods responsible for returning the feeds based
on IEnumerable<FeedItem> feed items.
You can also add the
MediaTypeHeadeValues for RSS and Atom and WriteToStreamAsync. So that the
runtime will determine which formatter to use during the content negotiation
handshake based on the value returned by the CanReadType or CanWriteType methods
and also the supported media types.
public class CustomSyndicationFeedFormatter : MediaTypeFormatter
{
private readonly string atom = "application/atom+xml";
private readonly string rss = "application/rss+xml";
public CustomSyndicationFeedFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(atom));
SupportedMediaTypes.Add(new MediaTypeHeaderValue(rss));
}
public override bool CanWriteType(Type type)
{
if (type == typeof(FeedItem) || type == typeof(IEnumerable<feeditem>))
return true;
else
return false;
}
public override bool CanReadType(Type type)
{
return false;
}
public override Task WriteToStreamAsync(Type type, object value, Stream stream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext)
{
return Task.Factory.StartNew(() =>
{
if (type == typeof(FeedItem) || type == typeof(IEnumerable<feeditem>))
BuildSyndicationFeed(value, stream, 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("Custom Feeds")
};
if (models is IEnumerable<feeditem>)
{
var enumerator = ((IEnumerable<feeditem>)models).GetEnumerator();
while (enumerator.MoveNext())
{
items.Add(BuildSyndicationItem(enumerator.Current));
}
}
else
{
items.Add(BuildSyndicationItem((FeedItem)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(FeedItem feedItem)
{
var category = new SyndicationCategory { Label = feedItem.Category };
var item = new SyndicationItem()
{
Title = new TextSyndicationContent(feedItem.Title),
LastUpdatedTime = feedItem.CreatedAt,
Summary = new TextSyndicationContent(feedItem.Description) ,
};
item.Categories.Add(category);
item.Authors.Add(new SyndicationPerson() { Name = feedItem.CreatedBy });
return item;
}
}
Hosting the WebAPI with formatters
Below is the code snippet for Self hosting of WebAPI.
Create an instance of HttpSelfHostConfiguration , set the route map. Here we
are replacing the Services with custom assemby resolver which loads the
ProductsLibrary WebAPI Library.
You can notice below, we have added our custom
UriBasedRssAtomMediaTypeFormatter and CustomSyndicationFeedFormatter to
HttpSelfHostConfiguration's Formatters collection.
static void Main(string[] args)
{
var config = new HttpSelfHostConfiguration("http://localhost:8081");
config.Routes.MapHttpRoute(
"API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional });
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());
config.Formatters.Add(new UriBasedRssAtomMediaTypeFormatter());
config.Formatters.Add(new CustomSyndicationFeedFormatter());
using (HttpSelfHostServer server = new HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Listening for HTTP requests.");
Console.WriteLine("(Run the ClientApp project to send requests).");
Console.WriteLine();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
Client Requests for getting feeds
Below we will see how to make a Http client request to get the codeproject ,
Product feeds.
Create a HttpClient and set the base address.
static HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:8081");
Below is the code snippet for getting the CodeProject feeds. We are making
use of HttpClient instance to make a request and get the feeds. Before making
the request , we will have to set the DefaultRequestHeaders of HttpClient
instance so that the respective WebAPI formatters will understand that it needs
to call custom formatter to construct the Rss feeds and return it to the
client.
We are making a WebAPI Request to FeedsController , it knows how to handle
the request with the help of the UriBasedRssAtomMediaTypeFormatter it returns
the response.
private static void GetFeeds(string name)
{
try
{
string query = string.Format("api/feeds?name={0}", name);
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/rss+xml"));
var resp = client.GetAsync(query).Result;
resp.EnsureSuccessStatusCode();
var feeds = resp.Content.ReadAsStringAsync().Result;
Console.WriteLine(feeds);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Below is the code snippet for fetching the products feeds. We are making a
Http web request to ProductsApiController. The GetAllProductsFeeds method with
in the ProductsApiController will be executed , it returns the List of feed
items and thus the CustomSyndicationFeedFormatter handles formatting of
FeedItems to return the response based on the requested media type.
private static void GetAllProductsFeeds()
{
try
{
string query = string.Format("api/products");
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/rss+xml"));
var resp = client.GetAsync(query).Result;
resp.EnsureSuccessStatusCode();
var feeds = resp.Content.ReadAsStringAsync().Result;
Console.WriteLine(feeds);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Points of Interest
I truly enjoyed exploring and learning concepts of formatters. I have spent a good amount of
time in learning how the formatters works , how to build custom ones.
History
Version 1.0 - 03/08/2013 - Initial version with two real world example
formatters.