Click here to Skip to main content
Click here to Skip to main content

Implementing Custom Media Formatters in ASP.NET WebAPI

, 9 Mar 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
We will see how to implement custom media formatters in ASP.NET WebAPI with two real world examples

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 a 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 negotiation 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 derive from the base class MediaTypeFormatter. The formatter after deriving can override the CanReadType and ReadFromStreamAsync methods for supporting deserialization and CanWriteType and WriteToStreamAsync methods for supporting serialization of models into a given media type format.

In order to create a custom formatter, we can either extend MediaTypeFormatter or BufferedMediaTypeFormatter and override the 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 a picture that shows at a high level how custom formatters are used to format the HTTP Response before sending to the client.  

 

Background

Understanding of ASP.NET WebAPI. Please refer my beginners' article on WebAPI. It is very important to 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 overrided the WriteToStreamAsync so that provides outputting RSS or Atom responses.

Note: We are using Atom10FeedFormatter and Rss20FeedFormatter to format the HTTP response 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 assembly 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 an HTTP client request to get the CodeProject Product feeds.

Create an 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 the 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 and return 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. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ranjan.D
Web Developer
United States United States
Profile
 
Around 10 years of professional software development experience in analysis, design, development, testing and implementation of enterprise web applications for healthcare domain with good exposure to object-oriented design, software architectures, design patterns, test-driven development and agile practices.
 
In Brief
 
Analyse and create High Level , Detailed Design documents.
Use UML Modelling and create Use Cases , Class Diagram , Component Model , Deployment Diagram, Sequence Diagram in HLD.
 
Area of Working : Dedicated to Microsoft .NET Technologies
Experience with : C# , J2EE , J2ME, Windows Phone 8, Windows Store App
Proficient in: C# , XML , XHTML, XML, HTML5, Javascript, Jquery, CSS, SQL, LINQ, EF
 
Software Development
 
Database: Microsoft SQL Server, FoxPro
Development Frameworks: Microsoft .NET 1.1, 2.0, 3.5, 4.5
UI: Windows Forms, Windows Presentation Foundation, ASP.NET Web Forms and ASP.NET MVC3, MVC4
Coding: WinForm , Web Development, Windows Phone, WinRT Programming, WCF, WebAPI
 
Healthcare Domain Experience
 
CCD, CCR, QRDA, HIE, HL7 V3, Healthcare Interoperability
 
Others:
 
TTD, BDD
 
Education
 
B.E (Computer Science)
 
CodeProject Contest So Far:
 
1. Windows Azure Developer Contest - HealthReunion - A Windows Azure based healthcare product , link - http://www.codeproject.com/Articles/582535/HealthReunion-A-Windows-Azure-based-healthcare-pro
 
2. DnB Developer Contest - DNB Business Lookup and Analytics , link - http://www.codeproject.com/Articles/618344/DNB-Business-Lookup-and-Analytics
 
3. Intel Ultrabook Contest - Journey from development, code signing to publishing my App to Intel AppUp , link - http://www.codeproject.com/Articles/517482/Journey-from-development-code-signing-to-publishin
 
4. Intel App Innovation Contest 2013 - eHealthCare - http://www.codeproject.com/Articles/635815/eHealthCare
 
5. Grand Prize Winner of CodeProject HTML5 &CSS3 Article Contest 2014
 
6. Grand Prize Winner of CodeProject Android Article Contest 2014

Comments and Discussions

 
GeneralMy vote of 4 PinmemberCarelAgain10-Mar-13 11:26 
GeneralRe: My vote of 4 PinmemberRanjan.D10-Mar-13 13:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 9 Mar 2013
Article Copyright 2013 by Ranjan.D
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid