What Will I Learn?
In this tutorial we only covered one new feature of ASP.NET MVC. We learned how to apply the new HttpMethodOverride helper and associated HTTP method attributes to achieve a convention-based RESTful set of routes for managing a resource in your ASP.NET MVC application.
Requirements
ASP.NET MVC Framework
Difficulty
Intermediate
Tutorial Contents
- REST-style Routes & Controllers
- RESTful Routing Rules
- PUT and DELETE from Javascript
- Possibilities
Implementing RESTful Routes & Controllers in ASP.NET MVC
ASP.NET MVC Preview 2 ships with two core pieces of functionality that can enable RESTful scenarios for your controllers and actions. If you aren't familiar with REST, don't worry.
REST is a complex topic, but the implementations are quite simple and straightforward.REST is an architectural style. REST stands for REpresentation State Transfer. Clear as mud, right? It actually makes sense when you read more about it. The core of a RESTful architecture is the resource. The resource is a noun, which represents some concept in your system. One of the first things you'll notice is that REST leverages lesser-known HTTP verbs like PUT and DELETE to describe operations that one would perform on a resource. The resource is addressable via URI, however the HTTP verb of the request is used to define what happens to the resource. An example is needed to fully explain this.
In our example last time we talked about a Contact. The contact would be your resource. The various actions on this could be:
- Get a Contact
- Create a new Contact
- Update an existing Contact
- Delete a Contact
If you recall, these were all accessible via separate action methods, all via HTTP GET or POST. In this tutorial we'll cover how to achieve the routing rules above leveraging new features in Preview 2. For more information on what REST is (including a lot of history) check out the Wikipedia page.
ASP.NET MVC Preview 2 introduces a couple new features that enable this functionality with minimal effort. First, is the HttpMethodOverride. Since today's browsers don't even speak PUT or DELETE, we have to use a little hack. By calling <%= Html.HttpMethodOverride(…) %> inside of a form, you are asking the framework to generate a hidden HTML input tag that coerces the framework to treat the form POST as some other verb. Preview 2 also provides new attributes that we can apply to our action methods, to restrict which HTTP verbs we allow to call a given action.
You already know about [AcceptVerbs(HttpVerbs.POST)]. The new attributes are of the same idea, however much cleaner: [HttpGet], [HttpPost], [HttpPut], [HttpDelete]. Apply these attributes to an action method to restrain the action to only those verbs.
Let's say we want to have RESTful controllers & actions by convention. The first piece of code we'll have to visit is our routing rules.
RESTful Routing Rules
It's clear that the default {controller}/{action}/{id} rules will not work for our RESTful controllers. We'll have to come up with a new set of rules that understand the different HTTP verbs we plan to use.
Before we create our routes, let's examine how they might look. As you saw in the table above, there are a few URLs that are identical, yet they differ in behavior based on which HTTP verb is used. We can use a route constraint for this.
The astute reader will already be thinking of using the built-in HttpMethodConstraint. The only problem is that it doesn't handle outbound routing at all. It only constrains requests coming in (inbound routing) and the route does not match if the request's HttpMethod property is contained in the allowed values of the constraint. Since this class is defined in System.Web.Routing and thus part of the .NET Framework, don't expect this to change any time soon.
What is outbound routing?
Outbound routing refers to the framework generating URLs based on a set of route values. It is generally not a good idea to hardcode URLs in your views. Any time you use Html.ActionLink, Html.BeginForm, or Url.Action the framework is using the routing rules to generate a URL for you. This is called outbound routing and it leverages the exact same set of routes that are used during incoming requests to map a URL to a controller & action pair.
Let's create a quick custom constraint that will fix this minor issue. We'll have the default behavior if the URL is an inbound request, but if it is an outbound request (RouteDirection.UrlGeneration) then we'll look for a custom route value called "httpMethod." If this is found, it will be used to match the values.
1: A custom class that adds outbound routing behavior to HttpMethodConstraint
public class RestfulHttpMethodConstraint : HttpMethodConstraint
{
public RestfulHttpMethodConstraint(params string[] allowedMethods)
:base(allowedMethods)
{
}
protected override bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
if(routeDirection == RouteDirection.UrlGeneration)
{
string verb = "GET";
if (values.ContainsKey(parameterName))
verb = values[parameterName].ToString();
return AllowedMethods.Any(v =>
v.Equals(verb, StringComparison.OrdinalIgnoreCase));
}
return base.Match(httpContext, route, parameterName, values, routeDirection);
}
}
Armed with this constraint, we can create routes for common URLs that will work with inbound and outbound routing. Since all of the routes we'll need to satisfy the table above are fairly common, and really the only varying piece is the controller and the prefix for the URLs, we can package them all up inside one reusable method.Here is our new route definition:
routes.MapResource<ContactController>("contacts");
The MapResource<T> method is an extension method that packages up all of these routes:
2: A reusable extension method for registering the RESTful routes
public static void MapResource<TController>(this RouteCollection routes, string alias) where TController :
IController
{
string controllerName = typeof(TController).Name.Replace("Controller", "");
//LIST
routes.MapRoute(alias + "_LIST", alias,
new { controller = controllerName, action = "index" },
new { httpMethod = new RestfulHttpMethodConstraint("GET") });
//NEW
routes.MapRoute(alias + "_NEW", alias + "/new",
new { controller = controllerName, action = "new" },
new { httpMethod = new RestfulHttpMethodConstraint("GET") });
//POST (create)
routes.MapRoute(alias + "_POST", alias,
new { controller = controllerName, action = "create" },
new { httpMethod = new RestfulHttpMethodConstraint("POST") });
//EDIT
routes.MapRoute(alias + "_EDIT", alias + "/{id}/edit",
new { controller = controllerName, action = "edit" },
new { httpMethod = new RestfulHttpMethodConstraint("GET") });
//PUT (update)
routes.MapRoute(alias + "_PUT", alias + "/{id}",
new { controller = controllerName, action = "update" },
new { httpMethod = new RestfulHttpMethodConstraint("PUT") });
//DELETE
routes.MapRoute(alias + "_DELETE", alias + "/{id}",
new { controller = controllerName, action = "delete" },
new { httpMethod = new RestfulHttpMethodConstraint("DELETE") });
//SHOW
routes.MapRoute(alias + "_SHOW", alias + "/{id}",
new { controller = controllerName, action = "show" },
new { httpMethod = new RestfulHttpMethodConstraint("GET") });
Each of these routes is defined with a custom name (though you'll never refer to these routes by name), and a URL that's based off of the alias you pass in. I did this because at times you want to have a different name for you URLs than what you name your controller. You might also go as far as to provide a singular and plural version of the resource alias, in order to get even cleaner looking routes.We need to be sure to place this route call above the default route. Leaving the default route in there ensures that other non-Restful controllers can still be addressed.
3: Global.asax – The default routing rule is left, as not all of our controllers will use the RESTful routes
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapResource<ContactController>("contacts");
routes.MapRoute("Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
So now we have all these routes pointing to specific action methods on our controllers. Let's extend this convention into a base controller class to make it easier on the controller developer. This way we can simply inherit and override.
4: A base class enforcing our RESTful conventions
public abstract class RestfulController<TEntity, TKey> : Controller
{
[HttpGet]
public abstract ActionResult Index();
[HttpGet]
public abstract ActionResult New();
[HttpGet]
public abstract ActionResult Show(TKey id);
[HttpPost]
public abstract ActionResult Create(TEntity entity);
[HttpGet]
public abstract ActionResult Edit(TKey id);
[HttpPut]
public abstract ActionResult Update(TEntity entity);
[HttpDelete]
public abstract ActionResult Delete(TKey id);
}
Since we've decorated each method with the appropriate HTTP verb attribute in the base class, we don't need to do this on the derived classes. Also, the abstract methods guarantee that derived classes will use the names that our routes are pointing to from before. Conventions can save you time, but you do have to know the convention to follow it. This base class allows us to be a bit lazy, as there's less to remember.We can now create our ContactController, derive from this base class, and implement all of the methods required.
5: An implementation of the RestfulController for Contacts
public class ContactController : RestfulController<Contact, int>
{
//fake the storage mechanism
private IContactRepository repository = new InMemoryContactRepository();
public override ActionResult Index()
{
var contacts = repository.List();
return View(contacts);
}
public override ActionResult Show(int id)
{
var contact = repository.Get(id);
return View(contact);
}
public override ActionResult New()
{
return View(new Contact());
}
public override ActionResult Create(Contact contact)
{
if(!ModelState.IsValid)
return View("New", contact);
repository.Add(contact);
TempData["Message"] = "The contact was added.";
return RedirectToAction("index");
}
public override ActionResult Edit(int id)
{
var contact = repository.Get(id);
return View(contact);
}
public override ActionResult Update(Contact contact)
{
if (!ModelState.IsValid)
return View("Edit", contact);
//update the properties of the contact
//(you could also do this in a smart model binder if you preferred)
var existingContact = repository.Get(contact.Id);
existingContact.FirstName = contact.FirstName;
existingContact.LastName = contact.LastName;
TempData["Message"] = "The contact was updated.";
return RedirectToAction("index");
}
public override ActionResult Delete(int id)
{
repository.Remove(id);
TempData["Message"] = "The contact was deleted.";
return RedirectToAction("index");
}
}
Our actions are concise and only consist of the abstract methods from the base class. For the curious, the guts of the InMemoryContactRepository are available below.
6: A quick, in memory repository for testing our our ContactController
internal class InMemoryContactRepository : IContactRepository
{
private static List<Contact> _contacts = new List<Contact>(new[] {
new Contact{Id=1, FirstName="Joe", LastName="Smith"},
new Contact{Id=2, FirstName="Jane", LastName="Brown"},
new Contact{Id=3, FirstName="John", LastName="White"},
new Contact{Id=4, FirstName="Jason", LastName="Black"},
new Contact{Id=5, FirstName="Jack", LastName="Harris"},
});
public IEnumerable<Contact> List()
{
return _contacts;
}
public void Add(Contact contact)
{
contact.Id = _contacts.Max(c => c.Id) + 1;
_contacts.Add(contact);
}
public Contact Get(int id)
{
return _contacts.Single(c => c.Id == id);
}
public void Remove(int id)
{
_contacts.RemoveAll(c => c.Id == id);
}
}
Let's focus on the ContactController's Index() action for now. It renders a view with a list of contacts. Here is the Index.aspx view.
7: Index.aspx (listing contacts)
<h2>Index</h2>
<%= Html.ActionLink("New Contact", "New") %>
<ul>
<% foreach(var contact in Model) { %>
<li><strong>
<%= Html.ActionLink(contact.Fullname, "show", "contact",new{id=contact.Id}, null) %>
</strong>
<%= Html.ActionLink("[edit]", "Edit", new{id=contact.Id}) %>
</li>
<% } %>
</ul>
You can see there is nothing spectacular here. This gives us a quick list of contacts. The new contact action & view are similar to what you've seen before. Where it does get interesting is the edit view.When you want to edit an existing resource, you'll need to utilize the HTTP verb PUT for our form. Likewise when you delete, you have to use the HTTP verb DELETE. We'll use both in our edit view.
8: Edit.aspx (editing or deleting an existing record)
<% using (Html.BeginForm("Update", "Contact", new{httpMethod="PUT"})) {%>
<%= Html.HttpMethodOverride(HttpVerbs.Put) %>
<fieldset>
<legend>Edit Contact</legend>
<p>
<%= Html.Hidden("Id") %>
<label for="FirstName">First Name:</label>
<%= Html.TextBox("FirstName") %>
<%= Html.ValidationMessage("FirstName", "*") %>
<label for="LastName">Last Name:</label>
<%= Html.TextBox("LastName") %>
<%= Html.ValidationMessage("LastName", "*") %>
</p>
<p>
<input type="submit" value="Update" />
</p>
</fieldset>
<% } %>
I've bolded the interesting parts of this form. First is the additional route value httpMethod that we're specifying on the BeginForm call. This is required for our outbound routing rule to generate the correct URL. If we omitted this, the framework would generate the wrong URL, and the action wouldn't work! If we look at the generated form, we can see that the URL is correct:
<form action="/contacts/1" id="form0" method="post">
Wait a minute! Didn't we specify the method to be PUT? Why does that form tag say POST? This is due to the simple fact that no modern browser supports PUT or DELETE in HTML forms. This might change in the future, but for now we're stuck with GET & POST. That doesn't stop us from faking it, which brings me to the second interesting part of this form: the new HttpMethodOverride call.
Why aren't PUT and DELETE supported in today's browsers?
In HTML 4 and XHTML 1, only GET and POST are supported via HTML forms. As we'll see later, you can perform a PUT and DELETE in JavaScript. Also we'll see with HTML 5 that browsers will start supporting PUT and DELETE (and perhaps more) HTTP verbs.This new html helper generates a hidden field that looks like this:
<input name="X-HTTP-Method-Override"2. 3.type="hidden" value="PUT" />
The ASP.NET MVC Framework will look for this when it verifies that an action can be called. Remember that [HttpPut] attribute we placed on the base class Update method? This attribute is verifying that the HTTP method of the request (slightly faked by the presence of this hidden form value) is equal to PUT.
Our Edit view & action are now working properly. It did require a slightly awkward tweak of using the custom route constraint and adding a route value to our BeginFormcall. These things could likely be automatically provided with a custom helper, which would be trivial to write. You might even automatically include the HtmlOverride call as well, but just be aware that you might be producing invalid XHTML if you don't wrap your form controls in some block level element like a <div> or <fieldset>. (Of course I didn't do this in my example either!)The delete button has a similar concept:
<% using(Html.BeginForm("delete", "contact", new{httpMethod="DELETE", id=Model.Id})) { %>
<%= Html.HttpMethodOverride(HttpVerbs.Delete) %>
<input type="submit" value="Delete this Contact" onclick="return confirm('Are you sure?')" />
<% } %>
The New view & action doesn't need these tweaks, as it is a simple form POST, and will work just the same as previous versions of the MVC framework.
PUT and DELETE from Javascript
I've mentioned that modern browsers don't support PUT and DELETE. This is true for HTML forms under HTML 4 and XHTML 1, which can only utilize GET and POST (POST being the most common). It is possible, however, to submit requests to the server using PUT & DELETE in an ajax request.For example, the following jQuery code will submit a PUT request to the server correctly:
$.ajax({
url:"/home/putsomething",
type:"PUT",
success: function() { alert("Success!") },
failure: function(e) { alert("ERROR: " + e); }
});
This can be used to access your PUT only actions for ajax requests. In Figure 1, you can see that a link has submitted an ajax request and the method is correctly marked as PUT.
Figure 1: Performing an ajax PUT request from jQuery
If you were to try this same URL as a GET or POST, you'd get a 404, since the action on the controller is marked as [HttpPost] and therefore invisible to the other methods.
Possibilities
One interesting possibility for these new RESTful controllers, actions, and routes would be to nest them. Rails has this concept, and it allows you to very easily address purely hierarchical content. For example, if you have a domain of artists, albums, and songs it makes perfect sense to nest them. Take a look at the following URL:
/artists/queens-of-the-stone-age/albums/songs-for-the-deaf/songs/02-no-one-
knows
If we pick this URL apart it is very clear that the data is nested. It's also easy to guess what type of result we were to get if we omitted a portion off the end of the URL. For example, if we remove the song name, we'll probably get a list of songs in that album. If we omit the album name we'll probably get a list of albums for that artist.Nested resources are absolutely possible, but it would probably take similar customization and conventions similar to what we accomplished in this article.
In this tutorial we only covered one new feature of ASP.NET MVC. We learned how to apply the new HttpMethodOverride helper and associated HTTP method attributes to achieve a convention-based RESTful set of routes for managing a resource in your ASP.NET MVC application. REST is a large topic, but if you follow the trends of some of the other web frameworks out there, REST is clearly becoming more and more popular.
Posted on Utopian.io - Rewarding Open Source Contributors