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.
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", "{controller}/{action}/{id}", new { controller = "Home", action = "Index",
id = UrlParameter.Optional } );
}
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:
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();
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);
ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
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; }
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:
[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:
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.
[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:
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:
[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.
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.
[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
.
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.
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.
[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
.
[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.