When getting started with ASP.NET MVC and/or the
ASP.NET Web API,
it can be overwhelming trying to figure out how it all works. These
frameworks offer powerful features, and abstract away a good deal of
pain associated with handling, routing, and responding to HTTP requests
within an application. This is a great thing for seasoned developers who
understand what it is the framework is doing "for" you (and how to
modify that behavior, if desired). It also makes it easier for new or
less-experienced folk to set up a basic site or API and watch it "just
work."
On the other hand, the abstraction can make it challenging for those
new to the MVC world to understand just what is going on, and where the
critical functionality they want to modify "lives."
One of the fundamental concepts to understand when using ASP.NET MVC
and/or the ASP.NET Web API is routing, which essentially defines how
your application will process and respond to incoming HTTP requests.
Image by Andrew Morton
Important note: This post covers the most basic
and fundamental concepts of routing as applied to the ASP.NET MVC
framework. The target audience are those with little or no familiarity
with routing in general, or who may be looking to review the
fundamentals. If you are an experience MVC developer, there is probably
not much here for you, although your feedback in the comments is greatly
appreciated.
UPDATE 9/26/2013 -
ASP.NET 5.0 and WebApi 2.0 introduce Attribute Routing as a standard
"out-of-the-box" feature. Attribute routing still follows most of the
patterns discussed here, but moves the route definitions out to the
controller methods they service. Attribute routing does not replace the
normal centralized route table discussed here, and in fact there is some contention about
what is the "one true way" to define routes. I will look more closely
at this new feature of ASP.NET in an upcoming post. Suffice it to say
there are differing architectural and design concerns implicit with both
approaches.
In my next post, having covered the fundamentals, I will examine route customization.
Routing Makes it All Work
Traditional web communication architecture maps a URL (
Uniform Resource Locator) to a file within the file system. For example, the following:
Collapse | Copy Code
http:
would tend to map to a file named
favorites.html, in the directory
~/mybooks/favorites, located in the root directory for the site
mydomain.com.
In response to an incoming HTTP request for this resource, the contents
of the file are either returned (as in the example above, as HTML) or
perhaps code associated with a file is executed (if, for example, the
file were a .aspx file).
Within the MVC framework,
as well as the Web API*,
URLs are instead mapped to specific methods which execute in response
to the incoming request, generally returning either a View (MVC) or some
sort of structured data (Web API) corresponding to the the requested
resource. In other words, instead of pointing to actual physical
resources within a file system, MVC and Web API routes instead point to
an abstraction which represents the resource requested, in both cases a
method which will return the requested item.
NOTE: There are some subtle differences between
MVC and Web API with respect to routing, but most of the concepts we
cover here are mutually applicable. I attempt to clarify Web Api Routing Specifics in a separate post.
This de-coupling of the URL from the physical file system allows us
to construct cleaner, more friendly URLs which are more beneficial to
the user, search-engine-friendly, and (in theory) more persistent,
meaning URLs associated with specific content are less likely to change,
and break incoming links. In the authoritative book
Professional ASP.NET MVC 4 , the authors refer to some common guidelines for high-quality URLs:
Usability expert Jacob Nielsen (
www.useit.com) urges developers to pay attention to URLs and provides the following guidelines for high-quality URLs. You should provide:
- A domain name that is easy to remember and easy to spell
- Short URLs
- Easy-to-type URLs
- URLs that reflect the site structure
- URLs that are hackable to allow users to move to higher levels of the information architecture by hacking off the end of the URL
- Persistent URLs which don't change
* Technically, routing is incorporated into ASP.NET generally, and is
available to all types of ASP.NET applications. However, the concept
has been largely associated with MVC. Further, Web API actually contains
its own implementation of routing, such the a Web API application can
be hosted independently of ASP.NET and IIS.
In MVC, the convention is to map URLs to a particular
action (a method) on a particular
controller. The action then executes, and (usually, but not always) returns an instance of
ActionResult
. The
ActionResult
class handles Framework logic such as rendering to HTML or JSON, and
writing to the HTTP response that will be returned to the user's
browser.
Once again, I defer to the authors of ASP.NET MVC 4 (who happen to also be members of the ASP.NET team):
"Routing within the ASP.NET MVC framework serves two main purposes:
- It matches incoming requests that would not otherwise match a file
on the file system and maps the requests to a controller action.
- It constructs outgoing URLs that correspond to controller actions
The most basic version of this convention would be a URL as follows:
Collapse | Copy Code
http://mydomain/controllername/methodname
In an MVC project, this is achieved by registering route templates
which establish how incoming URLs will be mapped to specific controllers
and actions. A typical MVC project defines a
Global.asx
file, which contains a single method –
Application_Start
.
Within this method, calls are made to various configuration methods to
set up the application's working state. One of these calls is to the
RegisterRoutes
method of the
RouteConfig
class found in the
App_Start
folder of the project.
Global.asx File and the RouteConfig File in a Typical MVC Project:
If we examine the
Global.asx file, we find the following code:
The Default ASP.NET MVC Global.asx File:
Collapse | Copy Code
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
For our purposes, we are interested only in the call to
RouteConfig.RegisterRoutes
. As we can see, the call passes the the
Routes
collection of the Global
RouteTable
as a parameter to the
RegisterRoutes
method, which then populates the routes collection with pre-defined
route templates for the application. The default MVC project template
comes with a single pre-configured route:
The Default MVC RouteConfig Class:
Collapse | Copy Code
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
}
}
Note that any MVC application must have at least one route definition
in order to function. In the above, a route template named "Default" is
added to the routes collection. The items in curly braces enclose
Route Parameters, and are represented by the parameter name as a placeholder between the curly braces.
Route Segments
are separated by forward slashes (much like a standard URL). Notice how
the implied relative URL our route specifies matches the MVC
convention:
Collapse | Copy Code
~/{controller}/{action}
Route parameters can be named just about anything, however ASP.NET
recognizes a few special route parameter names, in particular
{controller}
and
{action}
, and treats them differently than other route parameters.
When the routing framework encounters a route parameter named
{controller}
,
it appends the suffix "Controller" to the value of the parameter, and
then scans the project for a class by that name which also implements
the
System.Web.Mvc.IController
interface. Note that the search for a controller with a matching name is case-insensitive.
Once the framework has selected the proper controller, it attempts to
locate an action on the controller with a name matching the
{action}
parameter value. The search for a matching action name is
case-insensitive. If more than one action matches by name (as with
multiple overloaded methods on the same controller), the framework will
select the method for which the most URL parameters match method
arguments by name.
Additional URL Parameters other than
{controller}
and
{action}
are available to be passed as arguments to the selected Action method.
The framework will evaluate the input arguments of the available
actions, and match them by name (case-insensitively) to the URL
parameters other than
{action}
and
{controller}
. With certain restrictions, the framework will select that action with the greatest number of matching parameters.
Some things to consider:
- The MVC framework will first match method arguments by name to URL
parameters. Then, it will attempt to match any query string parameters
included in the URL by name. If the request is a POST, then the
framework will attempt to match the contents of the POST body.
- Method arguments are evaluated for a match by name only. The
framework does not consider the type required by the method argument.
For example, a URL parameter named
{id}
with a value of "John" will be considered a match for a method which accepts an int
argument named id
.
- Action methods can be decorated with attributes which restrict the
type of HTTP request they will respond to. Such attributes indicate the applicable HTTP verb to which the action will respond.
As an example of limiting the HTTP actions which a method may respond, consider the following:
Overloaded Action Method with HttpPost Attribute:
Collapse | Copy Code
public ActionResult Edit(int id)
{
return View();
}
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
return RedirectToAction("Index");
}
catch
{
return View();
}
}
In the above, we find two methods named
Edit
. The first accepts an
int
named
id
as an argument, and the second accepts an
int
named
id
and a
FormCollection
(a complex type). The purpose of this overloaded method is so that a browser can:
- Request a view with which to edit a record of some sort and then,
- Submit the modified record values back to the site for storage.
The first
Edit
method, which requires only an
int id
argument will be called using HTTP GET, and return a view with the
current representation of the data to be edited. Once the user has
updated values in the view and submits the form data, an HTTP POST
request is issued. The overloaded
Edit
method, decorated with the
[HttpPost]
attribute, is executed, and the modified data is persisted or otherwise processed.
The route mapping assumes that the URL template specified is relative to the
domain root for your site. In other words, since the entire application is hosted at
http://yourdomain.com it is not necessary to include this domain root as part of the route template.
In the case of the default MVC mapping from our
RouteConfig
class above, the route contains the two special parameters,
{controller}
and
{action}
. In processing incoming requests, the framework appends "Controller" to the value provided for the
{controller}
parameter, and then searches the project for a controller class of that
name. Once the proper controller has been identified, MVC next looks
for a method name corresponding to the value of the
{action}
parameter, and then attempts to match any additional parameters with input arguments accepted by that method.
For example, if our application receives a request with the following URL:
Collapse | Copy Code
http://mybookstore.com/books/details/25
the routing will match the default template. The string "Controller"
will be appended to the "books" segment, and the MVC framework will set
about searching the project for a class named
BooksController
. If the controller is located, MVC will then examine the controller for a public method named
Details
. If a
Details
method is found, MVC will attempt to find an overload which accepts a single argument named
id
, and then calls that method, passing in the final URL segment ("25" in this case) as an argument.
The following controller example would provide a suitable match for our incoming request:
A Simple Books Controller:
Collapse | Copy Code
public class BooksController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Details(int id)
{
return View();
}
}
The incoming request would result in a call to the
Details
method, passing in the integer
25
as the proper
id
argument. The method would return the appropriate view (how MVC does
this is another convention for another post – let's stay focused on
request routing for now).
Notice in the
RegisterRoutes
method, the registration of the "Default" route also appears to assign some default values to the
controller
and
action
,
parameters. These values will be used for any of these parameters if
they are missing from the incoming request URL. Additionally, the id
parameter is designated as optional. For example, consider the following
URL:
http://mybookstore.com/books/
In this case, we have specified the
Books
controller, but have not specified a value for the
action
or
id
. However, this route still matches our route definition, since MVC will provide the default value specified for the
action
parameter (in this case,
index
). Since the
id
parameter has been made optional in our route template, MVC will again search for a controller named
BooksController
, but in this case, examine the controller for a method named
Index
which does not require an argument. Again, a match is found, and the
Index
method is called, returning an appropriate view (most likely a list of all the books in the database).
The MVC default route mapping also specifies a default controller to
use when no controller parameter is specified; namely, the "Home"
controller. In other words, incoming requests to our domain root:
http://mybookstore.com/
will also match the default project controller. In this case, the MVC framework will attempt to locate a controller named
HomeController
, then locate the
Index
method of that controller. Since no
id
parameter was provided, the
Index
method will be called, returning the appropriate view ( most likely, the Homepage of our site).
What Next?
As we have seen above, MVC examines an incoming URL and attempts to
map each URL segment to a controller and action according to the route
templates set up in the
RouteConfig.MapRoutes
method. Once a proper controller and action have been identified, any additional URL segments (for example, the optional
{id}
segment in our example above) are evaluated against the action method
signature to determine the best parameter match for the action.
But what happens when we need to do more than just send an ID in as
an argument for the desired action method? Or, what if we have one or
more overloaded methods by which we wish to perform more complex queries
against our data?
While we can always include query parameters as part of our URL (and
in fact we will no doubt have to resort to this at various points in our
application design), we can customize and extend the default routing,
and exert a little more control over how how and what our application
will accept in an HTTP request by customizing our routes.
While the default
/controller/action/id
route baked into
the MVC project template is a useful start and will handle many common
controller cases, it is safe to say the MVC team did not expect
developers to limit their applications to this minimally-flexible,
single standard. Indeed, the ASP.NET routing framework (and the
corresponding routing framework used by Web API) are very flexible, and
within certain limits, highly customizable.
We'll look at route customization in the next post