REST services with MVC

02.05.2011 14:29Comments

First of all, you might be wondering why would I create a REST service with MVC when I can use WCF 4.0, WCF Starter Kit Preview 2 (deprecated), or even the new WCF Web APIs. The short answer is, because websites are, in practice, RESTful. So MVC has some interesting built in support for REST. So when thinking about MVC as a framework for REST services, we could actually ask ourselves “why not?”. As for the rest of the frameworks, each of them have also good reasons to be used, and I would say that you should use which ever fits best for your particular scenario.

Having said that, we must not overlook the fact that MVC has been released for websites (primarily), and as such we have excellent support for UI stuff, and we lack some support for other stuff we might need when building services. The lack of support for content negotiation, and media type based serialization is what I will discuss in this post.

Formatters

For the following snipets, I will be using this very simple interface for serializing/deserializing based on a content-type.

Code Snippet
  1. public interface IFormatter
  2. {
  3.     bool CanHandle(string contentType);
  4.     string Write(object data);
  5.     object Read(Type type, System.IO.Stream stream);
  6. }

Deserializing based on content type

Let’s look at one example

Code Snippet
  1. [HttpPost]
  2. public ActionResult Create(int id, Order order)
  3. {
  4.     order.Id = id;
  5.     return View(order);
  6. }

MVC will not deserialize parameters based on the content-type header of the HTTP request. This means that if we want to send an HTTP request with content-type “application/xml” and send an XML representation of the order, MVC will not know what to do with it. On the other hand, we must take into account, that the id parameter will most likely be bound to a Uri parameter using the routing service, and we don’t want to mess that up.

Another thing we don’t want to overlook, is the fact that when deserializing the body, we can only deserialize one parameter, the rest should come through other means (uri, headers, cookies, etc).

MVC does give us some interesting extensibility points where to accomplish this. When trying to deserialize the body of the request, we could use the help of a model binder. Each parameter of your Controller method, is a Model in MVC. And every model is “binded” (deserialized) using a Model binder. The most common binder we usually use (probably wihout knowing) is a binder which binds the parameters using the requested Uri based on the registered routes. Model binders in MVC are registered on the Global.asax file using the ModelBinders.Binders collection. One of the annoying things about registering a new IModelBinder is that you must register a type for which this binder will be used. This is not ideal, since in our case, we want to deserialize whatever we have in the body.

image

The other alternative we have is to add a CustomModelBinderAttribute on each attribute we want to deserialize using our media type based binder (take a look at this post for this approach).

MediaTypeBinder

In my case, I only want to use two types of parameters, Uri parameters based on routes, and the body parameter (when POSTing or PUTing). This means I will take a slightly different approach and assume that if the parameter is not in the route data, then it must come from the body. The easiest way to create a binder that is not associated to any particular type, and without loosing the out of the box binding functionality, is to inherit from DefaultModelBinder, and register our custom binder as the default. The code is very simple and uses the content-type header in the request to pick a formatter and do the deserialization.

Code Snippet
  1. public class MediaTypeBinder : DefaultModelBinder
  2.     {
  3.         private static readonly List<IFormatter> _formatters = new List<IFormatter>();
  4.  
  5.         public string DefaultType { get; set; }
  6.  
  7.         public MediaTypeBinder()
  8.         {
  9.             _formatters.Add(new XmlFormatter());
  10.             _formatters.Add(new JsonFormatter());
  11.         }
  12.  
  13.         public MediaTypeBinder RegisterFormatter(IFormatter formatter)
  14.         {
  15.             _formatters.Add(formatter);
  16.             return this;
  17.         }
  18.  
  19.         public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  20.         {
  21.             if (controllerContext.RouteData.Values.Keys.Contains(bindingContext.ModelName))
  22.             {
  23.                 return base.BindModel(controllerContext, bindingContext);
  24.             }
  25.             else
  26.             {
  27.                 return BindModel(controllerContext.HttpContext.Request.ContentType,
  28.                                     controllerContext.HttpContext.Request.InputStream,
  29.                                     bindingContext.ModelType);
  30.             }
  31.         }
  32.  
  33.         private object BindModel(string contentType, System.IO.Stream stream, Type type)
  34.         {
  35.             contentType = string.IsNullOrWhiteSpace(contentType) ? DefaultType : contentType;
  36.             var formatter = _formatters.FirstOrDefault(f => f.CanHandle(contentType));
  37.  
  38.             if (formatter == null)
  39.             {
  40.                 throw new InvalidOperationException("Content type not supported.");
  41.             }
  42.             else
  43.             {
  44.                 return formatter.Read(type, stream);
  45.             }
  46.         }
  47.     }

If the parameter is in the routed data, then we use the default binder. If it’s not, we use the right formatter based on the content type.

Serializing based on Accept header

Another thing about MVC is that it will pick up the right view for you and render the response using that view. In our case, we don’t need a conventional view, but rather a serialization of the response. This can be done using another extensibility point from MVC called ActionFilters. Action filters are attributes with which you can decorate the methods you want, or which you can register globally to run in every request. Action filters run before and after the controller action is invoked, so you can intercept the View that the controller returned, and perform your serialization on the View’s model.

The code is very similar to the Binder, except this time we are looking in the HTTP accept header for serialization.

Code Snippet
  1. public class MediaTypeFilter : ActionFilterAttribute
  2.     {
  3.         private static readonly List<IFormatter> _formatters = new List<IFormatter>();
  4.  
  5.         public string DefaultType { get; set; }
  6.  
  7.         // for deserialization
  8.         public MediaTypeFilter()
  9.         {
  10.             _formatters.Add(new XmlFormatter());
  11.             _formatters.Add(new JsonFormatter());
  12.         }
  13.  
  14.         public MediaTypeFilter RegisterFormatter(Formatters.IFormatter formatter)
  15.         {
  16.             _formatters.Add(formatter);
  17.             return this;
  18.         }
  19.  
  20.         public override void OnActionExecuted(ActionExecutedContext filterContext)
  21.         {
  22.             if (!(filterContext.Result is ViewResult)) return;
  23.  
  24.             var utf8 = new UTF8Encoding(false);
  25.             var data = ((ViewResult)filterContext.Result).ViewData.Model;
  26.             var acceptedTypes = filterContext.RequestContext.HttpContext.Request.AcceptTypes ?? new string[0];
  27.             
  28.             string contentType = DefaultType;
  29.             var formatter = GetFormatter(acceptedTypes, ref contentType);
  30.  
  31.             filterContext.Result = new ContentResult
  32.                     {
  33.                         ContentType = contentType,
  34.                         Content = formatter.Write(data),
  35.                         ContentEncoding = utf8
  36.                     };
  37.         }
  38.  
  39.         private IFormatter GetFormatter(string[] acceptedTypes, ref string contentType)
  40.         {
  41.             if (!acceptedTypes.Any())
  42.             {
  43.                 acceptedTypes = new[] { DefaultType };
  44.             }
  45.  
  46.             IFormatter formatter;
  47.             //pick the first formatter that can handle any accepted type.
  48.             var formatterMatch = _formatters
  49.                 .SelectMany(_ => acceptedTypes, (x, y) => new { Formatter = x, ContentType = y })
  50.                 .FirstOrDefault(x => x.Formatter.CanHandle(x.ContentType));
  51.  
  52.             if (formatterMatch != null)
  53.             {
  54.                 formatter = formatterMatch.Formatter;
  55.                 contentType = formatterMatch.ContentType;
  56.             }
  57.             else
  58.             {
  59.                 formatter = GetDefaultFormatter();
  60.             }
  61.             return formatter;
  62.         }
  63.  
  64.         private IFormatter GetDefaultFormatter()
  65.         {
  66.             return _formatters.First(f => f.CanHandle(DefaultType));
  67.         }
  68.  
  69.         public override void OnActionExecuting(ActionExecutingContext filterContext)
  70.         {
  71.             //do nothing. deserialization is being handled in the MediaTypeBinder.
  72.         }
  73.     }

Just so that you can have the entire implementation, I leave you the Xml and Json formatters.

Code Snippet
  1. public class JsonFormatter : IFormatter
  2. {
  3.     public bool CanHandle(string contentType)
  4.     {
  5.         return contentType.EndsWith("/json");
  6.     }
  7.  
  8.     public string Write(object data)
  9.     {
  10.         if (data == null)
  11.         {
  12.             return "{}";
  13.         }
  14.  
  15.         using (var stream = new MemoryStream())
  16.         {
  17.             JavaScriptSerializer js = new JavaScriptSerializer();
  18.             return  js.Serialize(data);
  19.         }
  20.     }
  21.  
  22.     public object Read(Type type, Stream stream)
  23.     {
  24.         return new JavaScriptSerializer().Deserialize(new StreamReader(stream).ReadToEnd(), type);
  25.     }
  26. }

Code Snippet
  1. public class XmlFormatter : IFormatter
  2. {
  3.     public bool CanHandle(string contentType)
  4.     {
  5.         return contentType.EndsWith("/xml");
  6.     }
  7.  
  8.     public string Write(object data)
  9.     {
  10.         UTF8Encoding utf8 = new UTF8Encoding(false);
  11.  
  12.         if (data == null)
  13.         {
  14.             return "";
  15.         }
  16.  
  17.         using (MemoryStream stream = new MemoryStream())
  18.         {
  19.             new XmlSerializer(data.GetType(), "").Serialize(stream, data);
  20.             return utf8.GetString(stream.ToArray());
  21.         }
  22.     }
  23.  
  24.     public object Read(Type type, Stream stream)
  25.     {
  26.         return new XmlSerializer(type).Deserialize(stream);
  27.     }
  28. }

Here’s how to set it up in the Global.asax.cs file.

Code Snippet
  1. protected void Application_Start()
  2. {
  3.     RegisterRoutes(RouteTable.Routes);
  4.  
  5.     GlobalFilters.Filters.Add(new MediaTypeBinder { DefaultType = "text/xml" });
  6.     ModelBinders.Binders.DefaultBinder = new MediaTypeBinder();
  7. }

So know you can make your controller actions media-type agnostic, and let something else worry about how to handle serialization.


comments powered by Disqus