|
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.
- What Makes a Good URL?
- How Routing Works in ASP.NET MVC
- Controller Matching
- Action Matching
- Action Parameter Matching
- Walk-Thru: the Default MVC Routing Template
- Route Parameter Defaults
- Other posts you may find useful
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:http://mydomain.com/mybooks/favorites.htmlwould 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
How Routing Works in ASP.NET MVC
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:The most basic version of this convention would be a URL as follows:
- 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
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:
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:
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:~/{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.
Controller Matching
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.
Action Matching
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.
Action Parameter Matching
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 anint
argument namedid
. - 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.
Overloaded Action Method with HttpPost Attribute:
public ActionResult Edit(int id)
{
return View();
}
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
// TODO: Add update logic here
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.
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 MVC Default Route Template – Routing Walk-Thru
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:
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:
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).
Route Parameter Defaults
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 theRouteConfig.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