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

Build Truly RESTful API and Website using Same ASP.NET MVC Code

By , 1 Feb 2012
Rate this:
Please Sign up or sign in to vote.
Summary.png

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 URLs like /Customers/Create or /Customers/John/Update, /Customers/John/Delete where the action is part of the URL that represents the entity. A 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 /Customers returns a 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 URI. You have to have URIs like http://localhost:8082/MovieApp/Home/Edit/5?format=Xml to edit a certain entity and define the format e.g. XML, that you need to support. They aren’t truly RESTful since the payload from the URI does not uniquely represent the state of an entity. The action has been made part of the URI. When you put the action on the URI, then it is straightforward to do it using ASP.NET MVC. Only when you take the action out of the URI and you have to support CRUD over the same URI, 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:

  • /Customers – returns a list of customers. A POST to this URL adds a new customer.
  • /Customers/C0001 – returns details of the customer having id C001. Update and Delete supported on the same URI.
  • /Customers/C0001/Orders – returns the orders of the specified customer. Post to this adds new order to the customer.
  • /Customers/C0001/Orders/O0001 – returns a specific order and allows update and delete on the same URL.

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.

Customers.png

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 example, /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 example, /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 example, first level if Customer and then second level is Orders. So, you need to register the routes in this way:

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 the SingleCustomer 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’s DataContractJsonSerializer.

internal class JsonResult2 : ActionResult
  {
    public JsonResult2() { }
    public JsonResult2(object data) { this.Data = data; }

    public string ContentType { get; set; }
    public Encoding ContentEncoding { get; set; }
    public object Data { get; set; }

    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 here and have made some modifications to support Generic types:

// 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 { get; set; }
    public Encoding ContentEncoding { get; set; }
    public object Data { get; set; }

    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:

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:

public class EnableXmlAttribute : ActionFilterAttribute
  {
    private readonly static string[] _xmlTypes = new string[] 
            { "application/xml", "text/xml" };

    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 (_xmlTypes.Any(type => acceptTypes.Contains(type)))
        filterContext.Result = new XmlResult()
        {
          Data = model,
          ContentEncoding = contentEncoding,
          ContentType = filterContext.HttpContext.Request.ContentType
        };
    }
  }

Both of these filters have the 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:

[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 the ASP.NET MVC 2 Futures library to allow JSON serialized objects to be sent as request. Futures has a JsonValueProvider 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:

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();

  RegisterRoutes(RouteTable.Routes);

  // Source: http://haacked.com/archive/2010/04/15/
  // sending-json-to-an-asp-net-mvc-action-method-argument.aspx
  // This must be added to accept JSON as request
  ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
  // This must be added to accept XML as request
  // Source: http://www.nogginbox.co.uk/blog/xml-to-asp.net-mvc-action-method
  ValueProviderFactories.Factories.Add(new XmlValueProviderFactory());
}

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 an ActionFilter 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.

The Model

Let’s quickly look at the model so that you understand how the code works. First we have a CustomerModel that holds a collection of Customers.

[DataContract]
public class CustomerModel
{
  [DataMember]
  public IEnumerable<Customer> Customers { get; set; }

Customer holds a collection of Orders.

[DataContract(Namespace="http://omaralzabir.com")]
public class Customer
{
  [Required]
  [DataMember]
  public string CustomerId { get; set; }
  
  [StringLength(50), Required]
  [DataMember]
  public string Name { get; set; }
  
  [StringLength(20), Required]
  [DataMember]    
  public string Country { get; set; }

  //[DataMember]
  public IEnumerable<Order> Orders
  {
    get;
    set; 
  }

Order looks like this:

[DataContract]
public class Order
{
  [Required]
  [DataMember]
  public string OrderId { get; set; }
  
  [StringLength(255), Required]
  [DataMember]
  public string ProductName { get; set; }
  
  [DataMember]
  public int ProductQuantity { get; set; }
  
  [DataMember]
  public double ProductPrice { get; set; }
}

That’s it.

Serving Collections

In order to serve collections like Customers and Orders, we need an action that returns a collection of object. For example, the Index action on CustomersController does this:

// GET /Customers
// Return all customers.
[EnableJson, EnableXml]
[HttpGet, OutputCache(NoStore=true, Location=OutputCacheLocation.None)]    
public ActionResult Index(string verb)
{
    return View(GetModel().Customers);
}

The EnableJson and EnableXml attributes are the two ActionFilter that I have made to support JSON and XML output. Then look at the request and see if JSON or XML is expected. If they are, they serialize the ViewModel into JSON or XML and return the serialized output instead of HTML.

The action method is not doing anything fancy here. It is just calling the view to render a collection of customers. The view takes the IEnumerable<Customer> and renders a table.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
    Inherits="System.Web.Mvc.ViewPage<IEnumerable<MvcRestApi.Models.Customer>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Index</h2>

    <h3>Customers</h3>
    <table>
        <thead>
            <th>Name</th>
            <th>Country</th>
            <th>Orders</th>
        </thead>

        <tbody>
        <% foreach (MvcRestApi.Models.Customer customer in Model)
           { %>
           <tr>
            <td><a href="Customers/<%= customer.CustomerId %>">
        <%= customer.Name %></a></td>
            <td><%= customer.Country %></td>
            <td><%= Html.RouteLink("Orders", "CustomerOrders", 
        new { customerID = customer.CustomerId }) %></td>
           </tr>
        <% } %>
        </tbody>
    </table>

    <p>
    <a href="?verb=New">Add New</a>
    </p>
    
<% Html.RenderPartial("Shared/XmlViewer"); %>

<% Html.RenderPartial("Shared/JsonViewer"); %>

</asp:Content>

The output is:

Customers.png

The page uses jQuery to hit the same URL /Customers with xml content type in order to get XML output. Then it makes hit to the same URL with json content type to get json output.

$.ajax({
      url: document.location.href,
      type: "GET",
      data: null,
      dataType: "xml",
      success: function (data) {
        renderXml(data);
      }
    });

And for json:

$.getJSON(document.location.href, function (data) {
  renderJson(data);
});

You can make changes on the JSON or XML manually and hit the respective post button and it will do a post to the same URL and show updates being done.

The best way to test the XML and JSON features is to see it from the individual entity page that is covered in the next section.

Serving collections that are under some entity is quite similar. You can create a similar action that takes the ID of the parent entity and then return the child collection of the entity. For example, the following action can return the collection under an entity as well as an individual item within the collection.

// GET /Customers/CUS0001/Orders(/ORD0001)
// Return customer orders. If orderId specified, then return a single order.
[EnableJson, EnableXml]    
[HttpGet, OutputCache(NoStore = true, Location = OutputCacheLocation.None)]    
public ActionResult SingleCustomerOrders(string customerId, string orderId)
{
  if (!string.IsNullOrEmpty(orderId))
    return View("SingleCustomerSingleOrder", GetModel()
      .Customers.First(c => c.CustomerId == customerId)
      .Orders.First(o => o.OrderId == orderId));
  else
    return View("SingleCustomerOrders", GetModel()
      .Customers.First(c => c.CustomerId == customerId)
      .Orders);

}

The above function returns orders for a customer. If an order ID is given, then it returns the specific order. It uses two different views to render orders collection and an individual order.

Serving Individual Entity

When you click on a Customer, you get this page:

SingleCustomer.png

It shows HTML, XML and JSON representation of a URL that represents a single Customer. You can update the details of the customer using HTML, XML or JSON method.

The action responsible for rendering this page and the XML and JSON representation is straightforward:

// GET /Customers/CUS0001
// Return a single customer data.
[EnableJson, EnableXml]
[HttpGet, OutputCache(NoStore = true, Location = OutputCacheLocation.None)]    
public ActionResult SingleCustomer(string customerId)
{
  var customer = GetModel().Customers.FirstOrDefault(c => c.CustomerId == customerId);
  if (customer == null)
    return new HttpNotFoundResult("Customer with ID: " + customerId + " not found");
  
  return View("SingleCustomer", customer);
}

The only interesting thing here is the HttpNotFoundResult that is thrown when any invalid customer ID is provided. The principle of REST is to return HTTP 404 code when a non-existent URL is hit, not HTTP 500. If we throw exception, it will become a HTTP 500. Thus a custom HttpNotFoundResult class is provided with the sample.

The view code that renders the HTML is also straightforward:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
    Inherits="System.Web.Mvc.ViewPage<MvcRestApi.Models.Customer>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    SingleCustomer
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <small><%= Html.ActionLink("<< Customers", "Index", "Customers") %></small>

    <h2>Customer Details</h2>
    <%= Html.ValidationSummary() %>

<% using (Html.BeginForm())
   { %>
   <fieldset title="Customer Details">
<p><%= Html.LabelFor(customer => customer.CustomerId)%>: 
    <%= Html.DisplayFor(customer => customer.CustomerId)%> </p>
<p><%= Html.LabelFor(customer => customer.Name)%>: 
    <%= Html.TextBoxFor(customer => customer.Name)%> 
    <%= Html.ValidationMessageFor(customer => customer.Name) %></p>
<p><%= Html.LabelFor(customer => customer.Country)%>: 
    <%= Html.TextBoxFor(customer => customer.Country)%> 
    <%= Html.ValidationMessageFor(customer => customer.Country) %></p>
    </fieldset>
<input type="submit" name="verb" value="Save" /> 
    <input type="submit" name="verb" value="Delete" />
<% } %>

<%= ViewData["Message"] ?? "" %>

<p>
<%= Html.RouteLink("Orders", "CustomerOrders", new { customerId = Model.CustomerId })%>
</p>

<% Html.RenderPartial("Shared/XmlViewer"); %>

<% Html.RenderPartial("Shared/JsonViewer"); %>
</asp:Content>

This view renders the HTML representation. Then on the UI, you have the XML and JSON test tool which you can use to make POST using XML and JSON. You can use the tools to perform updates on the entity.

SingleCustomerXMLPost.png

You can manually change the content of the XML and click the POST button.

When a POST happens to the entity URL, the SingleCustomer supporting HTTP Post gets fired:

// POST /Customers/CUS0001(?verb=Delete)
// Update/Delete a single customer
[HttpPost]
[EnableJson, EnableXml]
public ActionResult SingleCustomer
    (Customer changeCustomer, string customerId, string verb)
{
  if (verb == "Delete")
  {
    return SingleCustomerDelete(customerId);
  }
  else
  {
    if (ModelState.IsValid)
    {
      var existingCustomer = GetModel().Customers.First(c =>

This same code works for form post, XML and JSON post. The post also supports a query string of ?verb=DELETE in case there’s no way to send DELETE as HTTP method due to firewall or webserver filtering.

Adding New Entity to a Collection

This requires some trick when we have to do it over POST. In order to add a new entity, the common practice is to make a POST/PUT to the URL that represents the container collection. So, if you want to add a new customer, you make a post to /Customers, if you want to add a new order, you make a post to /Customers/CUS0001/Orders/.

Now supporting this over XML and JSON post is easy. But rendering the HTML UI on the same URL and accepting a post requires some trick. You need to pass some query string parameter on the URL to tell the action that you need the UI for adding a new entity, not the UI that renders the collection. This means you hit a URL /Customers?verb=New in order to get the HTML UI for new customer.

This is done by tweaking the Index action to take the additional verb as query parameter.

// GET /Customers
// Return all customers.
[EnableJson, EnableXml]
[HttpGet, OutputCache(NoStore=true, Location=OutputCacheLocation.None)]    
public ActionResult Index(string verb)
{
  if (verb == "New")
    return View("NewCustomer", new Customer());
  else
    return View(GetModel().Customers);
}

This renders a new view for creating a new customer.

NewCustomer.png

You can make a post to the URL and get the new customer added. You can also make XML and JSON post after putting values in the xml/json payload and get a new customer added.

NewCustomerJson.png

Now to add the new entity, we need a new action to listen to the same URL as the collection, but do the adding job.

// POST /Customers
// Add a new customer.
[EnableJson, EnableXml]
[HttpPost, OutputCache(NoStore = true, Location = OutputCacheLocation.None)]
[ActionName("Index")]
public ActionResult AddNewCustomer(Customer newCustomer)
{
  List<Customer> customers = new List<Customer>(GetModel().Customers);
  newCustomer.CustomerId = "CUS" + customers.Count.ToString("0000");
  customers.Add(newCustomer);
  GetModel().Customers = customers;

  return RedirectToAction("SingleCustomer", new { customerId = newCustomer.CustomerId });
}

The trick here is the [ActionName] attribute that says, “I am the same as the Index action listening to the /Customers url, but I do a different job”.

Deleting an Entity

In order to support DELETE HTTP method on a URL that represent a single entity, you need an action that listens to the same URL of the entity but accepts HTTP method DELETE.

// DELETE /Customers/CUS0001
// Delete a single customer.
[EnableJson, EnableXml]
[HttpDelete]
[ActionName("SingleCustomer")]
public ActionResult SingleCustomerDelete(string customerId)
{
  List<Customer> customers = new List<Customer>(GetModel().Customers);
  customers.Remove(customers.Find(c => c.CustomerId == customerId));
  GetModel().Customers = customers;
  return RedirectToAction("Index", "Customers");
}

Here the [ActionName] attribute says, “I am the same as SingleCustomer action listening to individual entity URLs, but I do different job”. The [HttpDelete] attribute makes it accept DELETE /Customers/CUS0001 requests.

Conclusion

The same code contains a library project that has the necessary value providers and action filters that enabled JSON, XML and HTML get and post over the same URL. Thus you can build a website and a web API using the same ASP.NET MVC code.

Plea for Charity

License

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

About the Author


Comments and Discussions

 
QuestionDoes ASP.NET MVC 4 changes anything from your sample? PinmemberDavid Martins1-Nov-12 1:29 

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 | Mobile
Web04 | 2.8.140415.2 | Last Updated 1 Feb 2012
Article Copyright 2011 by Omar Al Zabir
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid