ASP.NET MVC
ASP.NET, ASP.NET MVC
7/29/2011

Introduction

A truly RESTful API means you have unique URLs to uniquely represent entities and collections, and there is no verb/action on the URL. You cannot have URL like /Customers/Create or /Customers/John/Update,/Customers/John/Delete where the action is part of the URL that represents the entity. An URL can only represent the state of an entity, like /Customers/John represents the state of John, a customer, and allow GET, POST, PUT, DELETE on that very URL to perform CRUD operations. Same goes for a collection where /Customersreturns list of customers and a POST to that URL adds new customer(s). Usually we create separate controllers to deal with API part of the website but I will show you how you can create both RESTful website and API using the same controller code working over the exact same URL that a browser can use to browse through the website and a client application can perform CRUD operations on the entities.

I have tried Scott Gu’s examples on creating RESTful routes, this MSDN Magazine article, Phil Haack’s REST SDK for ASP.NET MVC, and various other examples. But they have all made the same classic mistake - the action is part of the URL. You have to have URLs like http://localhost:8082/MovieApp/Home/Edit/5?format=Xml to edit a certain entity and define the format eg xml, that you need to support. They aren’t truly RESTful since the URL does not uniquely represent the state of an entity. The action is part of the URL. When you put the action on the URL, then it is straightforward to do it using ASP.NET MVC. Only when you take the action out of the URL and you have to support CRUD over the same URL, using three different formats – html, xml and json, it becomes tricky and you need some custom filters to do the job. It’s not very tricky though, you just need to keep in mind your controller actions are serving multiple formats and design your website in a certain way that makes it API friendly. You make the website URLs look like API URL.

The example code has a library of ActionFilterAttribute and ValurProvider that make it possible to serve and accept html, json and xml over the same URL. A regular browser gets html output, an AJAX call expecting json gets json response and an XmlHttp call gets xml response.

You might ask why not use WCF REST SDK? The idea is to reuse the same logic to retrieve models and emit html, json, xml all from the same code so that we do not have to duplicate logic in the website and then in the API. If we use WCF REST SDK, you have to create a WCF API layer that replicates the model handling logic in the controllers.

The example shown here offers the following RESTful URLs:

All these URLs support GET, POST, PUT, DELETE. Users can browse to these URLs and get html page rendered. Client apps can make AJAX calls to these URLs to perform CRUD on these. Thus making a truly RESTful API and website.

They also support verbs over POST in case you don’t have PUT, DELETE allowed on your webserver or through firewalls. They are usually disabled by default in most webservers and firewalls due to security common practices. In that case you can use POST and pass the verb as query string. For ex, /Customers/C0001?verb=Delete to delete the customer. This does not break the RESTfulness since the URL /Customers/C0001 is still uniquely identifying the entity. You are passing additional context on the URL. Query strings are also used to do filtering, sorting operations on REST URLs. For ex, /Customers?filter=John&sort=Location&limit=100 tells the server to return a filtered, sorted, and paged collection of customers.

Registering routes for truly RESTful URLs

For each level of entity in the hierarchical entity model, you need to register a route that serves both the collection of an entity and the individual entity. For ex, first level if Customer and then second level is Orders. So, you need to register the routes in this way:

 

C#
Edit|Remove
public static void RegisterRoutes(RouteCollection routes) 
{ 
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 
 
  routes.MapRoute( 
    "SingleCustomer", 
    "Customers/{customerId}", 
    new { controller = "Customers", action = "SingleCustomer" }); 
 
  routes.MapRoute( 
    "CustomerOrders", 
    "Customers/{customerId}/Orders/{orderId}", 
    new { controller = "Customers", action = "SingleCustomerOrders", orderId = UrlParameter.Optional });       
 
  routes.MapRoute( 
    "Default"// Route name 
    "{controller}/{action}/{id}"// URL with parameters 
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults 
  ); 
}

 

The default map takes care of hits to /Customers. It calls Index() action on CustomersController. Index action renders the collection of customers. Hits to individual customers like /Customers/C0001 is handled by theSingleCustomer route. Hits to a customer’s orders /Customers/C001/Orders and to individual orders eg/Customers/C001/Orders/O0001 are both handled by the second route CustomerOrders.

Rendering JSON and XML output from actions

In order to emit JSON and XML from actions, you need to use some custom ActionFilter. ASP.NET MVC comes with JsonResult, but it uses the deprecated JavascriptSerializer. So, I have made one using .NET 3.5’sDataContractJsonSerializer.

 

C#
Edit|Remove
internal class JsonResult2 : ActionResult 
  { 
    public JsonResult2() { } 
    public JsonResult2(object data) { this.Data = data; } 
 
    public string ContentType { getset; } 
    public Encoding ContentEncoding { getset; } 
    public object Data { getset; } 
 
    public override void ExecuteResult(ControllerContext context) 
    { 
      if (context == null) 
        throw new ArgumentNullException("context"); 
 
      HttpResponseBase response = context.HttpContext.Response; 
      if (!string.IsNullOrEmpty(this.ContentType)) 
        response.ContentType = this.ContentType; 
      else 
        response.ContentType = "application/json"; 
 
      if (this.ContentEncoding != null) 
        response.ContentEncoding = this.ContentEncoding; 
 
      DataContractJsonSerializer serializer = new DataContractJsonSerializer(this.Data.GetType()); 
      serializer.WriteObject(response.OutputStream, this.Data); 
    } 
  }

 

In the same way I have created XmlResult that I found from from here and have made some modifications to support Generic types:

C#
Edit|Remove
// Source: http://www.hackersbasement.com/csharp/post/2009/06/07/XmlResult-for-ASPNet-MVC.aspx 
  internal class XmlResult : ActionResult 
  { 
    public XmlResult() { } 
    public XmlResult(object data) { this.Data = data; } 
 
    public string ContentType { getset; } 
    public Encoding ContentEncoding { getset; } 
    public object Data { getset; } 
 
    public override void ExecuteResult(ControllerContext context) 
    { 
      if (context == null) 
        throw new ArgumentNullException("context"); 
 
      HttpResponseBase response = context.HttpContext.Response; 
      if (!string.IsNullOrEmpty(this.ContentType)) 
        response.ContentType = this.ContentType; 
      else 
        response.ContentType = "text/xml"; 
 
      if (this.ContentEncoding != null) 
        response.ContentEncoding = this.ContentEncoding; 
 
      if (this.Data != null) 
      { 
        if (this.Data is XmlNode) 
          response.Write(((XmlNode)this.Data).OuterXml); 
        else if (this.Data is XNode) 
          response.Write(((XNode)this.Data).ToString()); 
        else 
        { 
          var dataType = this.Data.GetType(); 
          // OMAR: For generic types, use DataContractSerializer because  
          // XMLSerializer cannot serialize generic interface lists or types. 
          if (dataType.IsGenericType ||  
            dataType.GetCustomAttributes(typeof(DataContractAttribute), true).FirstOrDefault() != null) 
          { 
            var dSer = new DataContractSerializer(dataType); 
            dSer.WriteObject(response.OutputStream, this.Data); 
          } 
          else 
          { 
            var xSer = new XmlSerializer(dataType); 
            xSer.Serialize(response.OutputStream, this.Data); 
          } 
        } 
      } 
    } 
  } 

Now that we have the JsonResult2 and XmlResult, we need to create the ActionFilter attributes that will intercept the response and use the right Result class to render the result.

First we have the EnableJsonAttribute that emits JSON:

 

C#
Edit|Remove
public class EnableJsonAttribute : ActionFilterAttribute 
  { 
    private readonly static string[] _jsonTypes = new string[] { "application/json""text/json" }; 
     
    public override void OnActionExecuted(ActionExecutedContext filterContext) 
    { 
      if (typeof(RedirectToRouteResult).IsInstanceOfType(filterContext.Result)) 
        return; 
 
      var acceptTypes = filterContext.HttpContext.Request.AcceptTypes ?? new[] { "text/html" }; 
 
      var model = filterContext.Controller.ViewData.Model; 
 
      var contentEncoding = filterContext.HttpContext.Request.ContentEncoding ?? Encoding.UTF8; 
 
      if (_jsonTypes.Any(type => acceptTypes.Contains(type))) 
        filterContext.Result = new JsonResult2()  
        {  
          Data = model,  
          ContentEncoding = contentEncoding, 
          ContentType = filterContext.HttpContext.Request.ContentType 
        };       
    } 
  }

 

Then we have the EnableXmlAttribute that emits XML:

Both of these filters have same logic. They look at the requested content type. If they find the right content type, then do their job.

All you need to do is put these attributes on the Actions and they do their magic:

C#
Edit|Remove
[EnableJson, EnableXml] 
public ActionResult Index(string verb) 
{ 
    return View(GetModel().Customers); 
}

These filter work for GET, POST, PUT, DELETE and for single entities and collections.

Accepting JSON and XML serialized objects as request

ASP.NET MVC 2 out of the box does not support JSON or XML serialized objects in request. You need to use theASP.NET MVC 2 Futures library to allow JSON serialized objects to be sent as request. Futures has aJsonValueProvider that can accept JSON post and convert it to object. But there’s no ValueProvider for XML in the futures library. There’s one available here that I have used.

In order to enable JSON and XML in request

When both of these Value Providers are used, ASP.NET MVC can accept JSON and XML serialized objects as request and automatically deserialize them. Most importantly, ModelState.IsValid works. If you just use anActionFilter to intercept request and do the deserialization there, which is what most have tried, it does not validate the model. The model validation happens before the ActionFilter is hit. The only way so far to make model validation work is to use the value providers.

You can find a detail walkthrough of this project here:
http://omaralzabir.com/build-truly-restful-api-and-website-using-same-asp-net-mvc-code/