Posted on August 10th, 2011
ASP.NET MVC 3, with its glorious URL structures and ease of working with and controlling HTTP request/response data is primed to build REST type API services. But how does one accomplish that and what does the whole RESTful thing really mean?
Building a full blown API (of any type) involves a lot of architecture components, from data validation to security and beyond. This post does not attempt to address all of that. It focuses on the initial structure of a RESTful service within an ASP.NET MVC 3 application that works with JSON data in and out. We will look at how we can use the route engine, the HTTP verb attributes and a lean controller design to provide a starting point for a REST API.
We start by making use of Areas in MVC to create an API Area within an application. This will allow us to isolate the API and use another Area or even the top level to add documentation and other API support tools like a web interface for testing the API (outside the scope of this post).
Our sample API will handle Comment data. The class for a comment:
namespace Website.Areas.Api.Models
{
public class Comment
{
public int Id { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public string AuthorName { get; set; }
}
}
The API will support sending and receiving JSON structured data. We will be able to send in a Comment object in JSON like so:
{
"Subject": "A Subject",
"Body": "The Body",
"AuthorName": "Mike Jones"
}
or a batch of Comments:
{
items: [{
"Subject": "A Subject",
"Body": "The Body",
"AuthorName": "Mike Jones"
}, {
"Subject": "A Second Subject",
"Body": "The Other Body",
"AuthorName": "James Jones"
}]
}
Returned JSON for a single Comment will look like:
{
"Id": 3,
"Subject": "A Subject",
"Body": "The Body",
"AuthorName": "Mike Jones"
}
A set of Comments returned like so:
[{
"Id": 1,
"Subject": "A Subject",
"Body": "The Body",
"AuthorName": "Mike Jones"
}, {
"Id": 2,
"Subject": "A Second Subject",
"Body": "The Other Body",
"AuthorName": "James Jones"
}]
A brief REST before we route and control
To craft a RESTful solution there are a couple of targets that we want to hit. The first is the use of HTTP verbs to handle relative actions.
- GET Used to request data
- POST Used to create a new data record or a set of new data records
- PUT Used to update an existing data record
- DELETE Used to delete an existing data record
The second is to use a url structure that embodies a human readable request for data.
-
GET
/Api/Comments
/Api/Comments/2/10
/Api/Comments/Comment/3 -
POST
/Api/Comments/Comment -
PUT
/Api/Comments/Comment/3 -
DELETE
/Api/Comments/Comment/3
Routing
Within the area registration code (ApiAreaRegistration.cs
) we can add routes for our RESTful url patterns. Let's take a look at the code for the routes and then go over their purpose.
using System.Web.Mvc;
namespace Website.Areas.Api
{
public class ApiAreaRegistration : AreaRegistration
{
public override string AreaName { get { return "Api"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"SingleComment",
"Api/Comments/Comment/{id}",
new { controller = "Comments", action = "Comment",
id = UrlParameter.Optional }
);
context.MapRoute(
"ListComments",
"Api/Comments/{page}/{count}",
new { controller = "Comments", action = "CommentList",
page = UrlParameter.Optional, count = UrlParameter.Optional }
);
context.MapRoute(
"ListCommentsAll",
"Api/Comments",
new { controller = "Comments", action = "CommentList",
page = UrlParameter.Optional, count = UrlParameter.Optional }
);
context.MapRoute(
"Api_default",
"Api/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
Routes do not have any knowledge of the HTTP verbs, so the ones that we add are going to support multiple scenarios to get to our controller actions. The SingleComment route supports our GET, PUT and DELETE requests when an id is included in the url string, and the POST when the id is left off. The ListComments route supports a GET request for paging Comments in which a page number and a count per page is included in the url string. The ListCommentsAll route handles a GET request for all Comments (no paging). It will also support a POST request of a list of Comments where the url string does not include anything after the /Api/Comments
.
Before we take a look at the CommentsController
code, let's check out a custom ActionFilterAttribute
that we can craft to help us handle multiple verbs through a single controller action.
using System.Web.Mvc;
namespace Website.Models
{
public class RestHttpVerbFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var httpMethod = filterContext.HttpContext.Request.HttpMethod;
filterContext.ActionParameters["httpVerb"] = httpMethod;
base.OnActionExecuting(filterContext);
}
}
}
This code will capture the HTTP verb of the request and store it in the ActionParameters
collection. By applying this attribute to a controller action we can add a method parameter named httpVerb
and the RestHttpVerbFilter
will handle binding the HTTP request verb value to it. Our controller needs to support an action method with a common signature (the same parameters) but take different actions based on the HTTP verb. It is not possible to override a method with the same parameter signature but a different HTTP verb attribute. This custom attribute will allow us to have a single controller action method that can take action based on the HTTP verb without having to contain the logic to determine the verb. With a single controller this is not that big of a deal, but I'd imagine that our API is going to embody more than just Comment management, and thus a need to repeat the verb capture code in multiple controllers for the API.
Let's take a look at the CommentsController
code and see how this all unfolds:
using System.Collections.Generic;
using System.Web.Mvc;
using Website.Areas.Api.Models;
using Website.Models;
namespace Website.Areas.Api.Controllers
{
public class CommentsController : Controller
{
ICommentManager commentManager;
public CommentsController()
{
this.commentManager = new CommentManager();
}
[HttpGet]
public JsonResult CommentList(int? page, int? count)
{
var model = this.commentManager.GetComments(page, count);
return Json(model, JsonRequestBehavior.AllowGet);
}
[HttpPost]
public JsonResult CommentList(List items)
{
var model = this.commentManager.CreateComments(items);
return Json(model);
}
[RestHttpVerbFilter]
public JsonResult Comment(int? id, Comment item, string httpVerb)
{
switch(httpVerb)
{
case "POST":
return Json(this.commentManager.Create(item));
case "PUT":
return Json(this.commentManager.Update(item));
case "GET":
return Json(this.commentManager.GetById(id.GetValueOrDefault()),
JsonRequestBehavior.AllowGet);
case "DELETE":
return Json(this.commentManager.Delete(id.GetValueOrDefault()));
}
return Json(new { Error = true, Message = "Unknown HTTP verb" });
}
}
}
The controller has a private member of type ICommentManager
and the controller constructor instantiates an object of type CommentManager
that will implement the interface. The contract for the interface looks like so:
using System.Collections.Generic;
namespace Website.Areas.Api.Models
{
public interface ICommentManager
{
Comment Create(Comment item);
List CreateComments(List items);
Comment Update(Comment item);
Comment GetById(int id);
List GetComments(int? page, int? count);
bool Delete(int id);
}
}
The first controller action, CommentList(int? page, int? count)
, supports the HTTP GET verb only and handles querying comments. The logic to determine if the list of comments is paged or not will be brokered off to the CommentManager
. The returned List
data is sent to the JsonResult
which will handle serializing it to JSON and providing the correct response headers to the client consuming the API. This method is hit whenever a GET request is made with the following url string structures:
/Api/Comments
/Api/Comments/2/10
The next controller action, CommentList(List
, supports the HTTP POST verb only and is used to support adding multiple Comment objects in a single request. This method is hit whenever a POST request is made with the following url string structure:
/Api/Comments
The final controller action, Comment(int? id, Comment item, string httpVerb)
, is where most of the magic happens. This method supports all four verbs when a request is made with the following url string structures:
/Api/Comments/Comment
/Api/Comments/Comment/3
The Comment
method brokers all logic to work with a single comment to the appropriate CommentManager
method and returns the result of those methods directly through the JsonResult
. If, for some insane reason (HTML 6 arrives with new verbs), we receive an unsupported HTTP verb we return a custom JSON error object.
Testing the API with Fiddler
We can make use of Fiddler to test the API. First we need to create the CommentManager
class and give it some sample logic to return some test data.
using System.Collections.Generic;
namespace Website.Areas.Api.Models
{
public class CommentManager : ICommentManager
{
public Comment Create(Comment item)
{
item.Id = 1;
return item;
}
public List CreateComments(List items)
{
return new List { 1, 2, 3 };
}
public Comment Update(Comment item) { return item; }
public Comment GetById(int id)
{
return new Comment
{
Id = id,
Subject = "Loaded Subject",
Body = "Loaded Body",
AuthorName = "Loaded Author"
};
}
public List GetComments(int? page, int? count)
{
var comment1 = new Comment
{
Id = 1,
Subject = "First Subject",
Body = "First Body",
AuthorName = "First Author"
};
var comment2 = new Comment
{
Id = 2,
Subject = "Second Subject",
Body = "Second Body",
AuthorName = "Second Author"
};
var items = new List { comment1, comment2 };
return items;
}
public bool Delete(int id) { return true; }
}
}
Then we can F5 the project to have the API running, copy the localhost with port url and use that in Fiddler to send in HTTP requests. The url for my instance:
http://localhost:24771/
NOTE:
The sample code download has a defaultHomeController
class. I did not mention adding that in the article. If you are crafting the code as you read through it then you may get "The resource cannot be found" server error when running the debug. That's ok, you can still hit theCommentsController
with Fiddler. Just leave the browser with the server error open while you use Fiddler so the MVC application is running.
Creating a new request in Fiddler is done by clicking on the Request Builder tab. Leaving the drop down for the verb to GET and setting the url to http://localhost:24771/Api/Comments, we can execute it and see that application is hit (by the result in the Web Sessions panel on the left).
Selecting the session result, clicking on the Inspectors tab and then on the Raw view button allows us to view the response data.
The other GET requests are done the same, but with different urls. The POST, PUT and DELETE actions involve changing the HTTP verb drop down, using either the http://localhost:24771/Api/Comments url for a POST of multiple comments or the http://localhost:24771/Api/Comments/Comment/{id} url for working with a single comment. Within the Request Body text area we can add the JSON object that we want to send to the API. The only other piece that we need to handle is telling the API via the HTTP header that the content type is JSON. In the Request Headers text area we need to add the following line:
Content-Type: application/json
MVC will identify this header and use the JSON model binding to map the request body data to a Comment
object in our controller actions (or the List
object if we are doing a POST of multiple Comments at once). Posting a new comment to the API with Fiddler looks like so:
If we set breakpoints throughout the CommentsController
we can go through the various requests with Fiddler and validate that our routing is working as planned and that we are reaching the correct action methods for each type of request.
Can I hand out my API url now?
Remember, this is just a starting point for creating REST type functionality in an MVC 3 application. There is a long way to go to craft out a true API solution. The next step would be to fill out the logic for the CommentManager
to work with a data storage layer for persisting the Comment data. After that, adding some logic in the CommentsController.Comment
method to support clients that don't support the PUT or DELETE verbs. From there the fun begins. Thinking about security, data validation, standard error response support, cross domain support, and on and on.
Hey, writing software ain't like dustin' crops boy...'er, or girl. All smuggler's humor aside, if you are in need of a way to write a RESTful service within MVC 3 hopefully this can help get you rolling.
Download the code
Discussion
mitsbits
Just as I was wondering, should I do asmx or svc or should I use a controller for my json? This (as a lot of your posts) has been great help. Keep it up!
Justin Schwartzenberger
jie
Can this be used for Web Forms?
Justin Schwartzenberger
mitsbits
I can not state enough how well this worked with the Remote Attribute in DataAnnotations. Justin, thanks again. P.S. People switch to MVC now, the validation model alone is worth it!
Excellent post.
One of the best introduce to Restfull and MVC.
Thanks to make me lost my time getting so much experience in a single post.
Last but not least, your blog its well done formatted and easy to read.
Jaymie
Very very useful article. Easy to understand and very well put together :) I found myself being able to modify your code to create my own RESTful Api, so a big thanks for your help :)
George
One question I have is what if you want to call your MVC RESTful api from a windows forms client? Is this possible? What about from an iPhone app? Is there any way to generate a proxy class like you can in WCF with SVCUTIL.EXE? It seems to me that if you can't call the MVC api from anything but another web browser client, then it's kind of limiting. Am I missing something?
Justin Schwartzenberger
robin
Thanks buddy, this is a great introduction. Please consider doing a part 2,3,etc. about "security, data validation, standard error response support, cross domain support, and on and on." :)
Will K
Great article. I've developed RESTful api with ASP.NET MVC3 in my project for a period of time. I used to test my api. Every time when I changed the interface of api. It generates test page automatically for me. I found this very useful while I was changing the specification of api frequently in the development stage. I don't even need Fiddler.
Awesome tutorial. Thanx for the write up.
With my Resources Over MVC framework you can use an attribute to add RESTful support. If you don't want to use somebody else's framework, one idea you might want to take on board is adding a test harness directly into your API (i.e. if one of the formats you serve is HTML, add a little bit of JavaScript to allow a developer to experiment with your API from within the browser... I have an example here:
Justin Schwartzenberger
Ken Pierce
Thanks a bunch. This post really helped get me over a few stumbling blocks that have been plaguing me.
DeejayRaoul
Thanks for getting me started on building a REST service in MVC.
Is there any reason to use the filter to determine the Http method?
I would say this will do, since 'request' is available in your controller:
Justin Schwartzenberger
DeejayRaoul
@Justing: Thanks for the reply. I can see the use for more abstraction if needed. Generally i see my controller layer as a relatively thin/dumb layer to transer request to my Repository layer where i implement business logic and database interaction. Since i work on reletively straightforward web- project the need to independently test the controller layer hasn't arisen yet. Something to definitely consider in the future though.
PS: i linked to your article in my newly started blog about my adventures in codeland: http://deejayraoul.blogspot.com/2012/01/building-rest-service-in-mvc-net.html
Justin Schwartzenberger
zakizakaria69
Thanks for this great post. This is very smart approach and make it easy to return presentations as complete web services.
Steve Gray
Justin, what a great article. Excellent work, a great start. If you have any follow ups on security and the rest of the mix then please do post links here.
Fiddler rocks as well!
Thanls
Steve
Great Article,
I must agree that building out a REST API requires a bunch of components, after building a few for various projects and clients I realized there could be a better way.
Soon I will be launching a new product called RocketAPI that will allow developers to build and deploy a fully functional rest API without having to write a single line of code and without needing to config and manage any servers or hardware,
While its not quite ready yet, it will be soon and you will be able to signup and create API for free.
I really did find the article useful for developers, but felt I should also let everyone know about what we are doing.
John
Mau
Sweet! Although I still have some questions unanswered, but your post gives enough to start with my own project. Thanks a lot.
Great article, it was exacly I was looking for to implement. Thank you...
This is certainly a step forward from the ASP.NET AJAX 1.0 extensions that I'm used to. Can't wait to connect this to an ExtJS REST store!
Shanty
I'm tryin' to implement the same thing using MVC2 and IIS6 using this post
but I think that I have to change the controller