Wednesday, April 1, 2015

Routing in mvc

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.
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 experienced 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:
http://mydomain.com/mybooks/favorites.html
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 bookProfessional 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.

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:
  • 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:
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 theRouteConfig class found in the App_Start folder of the project.
Global.asx File and the RouteConfig File in a Typical MVC Project:
global-asx-and-route-config-files
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 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:
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.
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 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 Detailsmethod 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 idargument. 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 theaction 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.

Additional Resources


Routing is one of the primary aspects of the MVC framework which makes MVC what it is. While that is possibly over-simplifying, the routing framework is where the MVC philosophy of "convention over configuration" is readily apparent.
Routing also represents one of the more potentially confounding aspects of MVC. Once we move beyond the basics, as our applications become more complex, it generally becomes necessary to customize our routes beyond the simple flexible default MVC route.
Route customization is a complex topic. In this article, we will look at some of the basic ways we can modify the conventional MVC routing mechanism to suit the needs of an application which requires more flexibility. If you are new to MVC, you may want to reveiw Routing Basics in ASP.NET MVC as well.  
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.   
Note: Most of the examples in this post are somewhat contrived, and arbitrary. My goal is to demonstrate some basic route customization options without getting distracted by the business case-specifics under which they might be required. I recognize that in the real world, the examples may or may not represent reasonable cases for customized routes.
In any sufficiently complex ASP.NET MVC project, it is likely you will need to add one or more custom routes which either supplement or replace the conventional MVC route. As we learned in Routing Basics in ASP.NET MVC, the default MVC route mapping is:
http://<domain>/{controller}/{action}/{id}
This basic convention will handle a surprising number of potential routes. However, often there will be cases where more control is required, either to:
  • Route incoming requests to the proper application logic in complex scenarios.
  • Allow for well-structured URLs which reflect the structure of our site.
  • Create URL structures that are easy to type, and "hackable" in the sense that a user, on examining the structure of a current URL, might reasonably make some navigation guesses based on that structure.
Route customization is achieved in a number of ways, usually by combining modifications to the basic pattern of the route mapping, employing route default options to specify controllers and actions explicitly, and less frequently, using route constraints to limit the ways in which a route will "match" a particular URL segment or parameter.

URLs are Matched Against Routes in Order – The First Match Wins

Incoming URLs are compared to route patters in the order the patterns appear in the route dictionary (that is what we added the route maps to in our RouteConfig.cs file). The first route which successfully matches a controller, action, and action parameters to either the parameters in the URL or the defaults defined as part of the route map will call into the specified controller and action. This is important, and requires us to think our routes through carefully so that the wrong handler is not called inadvertently.
The basic process of route matching was covered in Routing Basics in ASP.NET MVC.

Modifying the URL Pattern

As we learned previously, in an MVC application, routes define patterns by which incoming URLs are matched to specific controllers, and actions (methods) on those controllers. Route mappings recognize URLs as a pattern of segments separated (or delimited) by a slash (/) character. Each segment may contain various combinations ofliterals (text values) and parameter placeholders. Parameter placeholders are identified in a route definition by braces.
MVC recognizes the special parameter placeholders {controller} and {action} and uses these to locate the appropriate controller and method to call in response to an incoming URL. In addition to these two special placeholders, we can add just about anything to a route as a parameter placeholder to suit our purpose, so long as we adhere to good URL design principles, and of course, reasonably expect the parameter to be useful.
We are familiar with the default MVC pattern shown above. We can see that the route is composed of three segments, with no literals, and parameter placeholders for the special parameters {controller} and {action}, as well as an additional parameter called {id}.
When creating route URL patterns, we can combine literals with parameter placeholders in any number of ways, so long as parameter placeholders are always separated by either a delimiter, or at least one literal character. The following are examples of valid route patterns (whether they are sensible or not is another issue):
Examples of Valid Route Patterns in ASP.NET MVC
Route PatternURL Example
mysite/{username}/{action}~/mysite/jatten/login
public/blog/{controller}-{action}/{postId}~/public/blog/posts-show/123
{country}-{lang}/{controller}/{action}/{id}~/us-en/products/show/123
products/buy/{productId}-{productName}~/products/but/2145-widgets
The following pattern is not valid, because the {controller} parameter placeholder and the {action}parameter placeholder are not separated by either a slash (/) or another literal character. In this case, the MVC framework has no way to know where one parameter ends, and the next begins (assume the controller is named "People" and the action is "Show"):
Route PatternURL Example
mysite/{controller}{action}/{id}~/mysite/peopleshow/5
Not all of the valid route examples above include either the {controller} parameter placeholder or the{action} parameter placeholder. The last example does not include either. Also, most include user-defined parameters, such as {username} or {country}. We'll take a look at both in the next two sections.

Modifying Route Default Options

In conjunction with modifying the route URL pattern, we can also take advantage of the route defaults to create routes which are more specific, and in some cases which map only to a specific controller and/or action.
The standard MVC project file contains the route mapping configuration in a file named RouteConfig.cs:
The standard MVC RouteConfig.cs File
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 }
        );
    }
}
The route above establishes a default value for both the controller and the action, in case one more both are not provided as part of an incoming URL.
We can take this a step further, and add a new, more specific route which, when it matches a specific URL pattern, calls into one specific controller. In the following, we have added a new route mapping in our RouteConfig.csfile:
Adding a More Restrictive Route
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    // ALL THE PROPERTIES:
    // rentalProperties/
    routes.MapRoute(
        name: "Properties",
        url: "RentalProperties/{action}/{id}",
        defaults: new
        {
            controller = "RentalProperty",
            action = "All",
            id = UrlParameter.Optional
        }
    );
 
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { 
            controller = "Home", 
            action = "Index", 
            id = UrlParameter.Optional }
    );
}
First off, take note that we have added the new, more specific route before the default in the file (which determines the order it is added to the route dictionary). This is because routes are evaluated for a match to an incoming URL in order. The default MVC route is pretty general, and if route order is not carefully considered when adding additional routes to your project, the default route may inadvertently match a URL intended for a different handler.
Beyond this, what we see in the new route added above is that there is no {controller} parameter placeholder in the URL pattern of the "Properties" route. Since no controller can be passed to this route as a parameter, it will always map to the RentalPropertiesController , and to whichever Action is provided as a parameter. Since there is a default action defined, if the incoming URL matches the route but does not contain an Actionparameter, the default All() method will be called.

A Contrived Example

So far, in this rather contrived example, we haven't accomplished anything we couldn't have done using the default MVC route pattern. However, suppose in our application we wanted to define the following URL patterns and associated URL examples for accessing views in a property management application:
Desired URL patterns for Simple Property Management Application
BehaviorURL Example
Show all the rental properties~/rentalproperties/
Show a specific rental property~/rentalproperties/propertyname/
Show a specific unit at a property~/rentalproperties/propertyname/units/unitNo
We could define a RentalPropertyController class as follows:
Rental Property Controller Example
public class RentalPropertiesController : Controller
{
    private RentalPropertyTestData _data = new RentalPropertyTestData();
 
    // List all the properties
    public ActionResult All()
    {
        var allRentalProperties = _data.RentalProperties;
        return View(allRentalProperties);
    }
 
 
    // get a specific property, display details and list all units:
    public ActionResult RentalProperty(string rentalPropertyName)
    {
        var rentalProperty = _data.RentalProperties.Find(a => a.Name == rentalPropertyName);
        return View(rentalProperty);
    }
 
 
    // get a specific unit at a specific property:
    public ActionResult Unit(string rentalPropertyName, string unitNo)
    {
        var unit = _data.Units.Find(u => u.RentalProperty.Name == rentalPropertyName);
        return View(unit);
    }
}
Given the above controller, and the desired URL patterns in the previous table, we can see that the default MVC route mapping would work for some, but not all of our URLs. We might be better served by adding the following controller-and-action-specific routes to our application, so that our RouteConfig.cs file looks like this:
Modified Route Config File for Property Management Example Application
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    // RentalProperties/Boardwalk/Units/4A
    routes.MapRoute(
        name: "RentalPropertyUnit",
        url: "RentalProperties/{rentalPropertyName}/Units/{unitNo}",
        defaults: new
        {
            controller = "RentalProperties",
            action = "Unit",
        }
    );
 
    // RentalProperties/Boardwalk
    routes.MapRoute(
        name: "RentalProperty",
        url: "RentalProperties/{rentalPropertyName}",
        defaults: new
        {
            controller = "RentalProperties",
            action = "RentalProperty",
        }
    );
 
    // RentalProperties/
    routes.MapRoute(
        name: "RentalProperties",
        url: "RentalProperties",
        defaults: new
        {
            controller = "RentalProperties",
            action = "All",
            id = UrlParameter.Optional
        }
    );
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new
        {
            controller = "Home",
            action = "Index",
            id = UrlParameter.Optional
        }
    );
}
As we see now, all of the newly added routes explicitly specify both the controller and action, and allow for our progressively enhanced URL structure which includes the property name as part of the URL.
Of course, in the real world, this URL scheme would likely not work the way we desire, because (depending upon how our database is going to be used) the names of rental properties may not be unique, even within a given management company. If the company operated rental properties in several states or even counties within a state, it is entirely possible that there would be more than one property named (for example) "Maple Glen." It is a sad fact that despite all the wonderfully simple examples we can find while learning, the real world often fails to model in the way we want.

Adding Route Constraints

Route constraints can be used to further restrict the URLs that can be considered a match for a specific route. While we have so far examined whether routes match based upon URL structure and parameter names, Route Constraints allow us to add some additional specificity.
The MVC framework recognizes two types of constraints, either a string, or a class which implements the interface IRouteConstraint.

Regular Expressions in Route Constraints

When a string is provided as a constraint, the MVC framework interprets the string as a Regular Expression, which can be employed to limit the ways a URL might match a particular route. A common scenario in this regard would be to restrict the value of a route parameter to numeric values.
Consider the following route mapping:
Example Route Mapping for Blog Application with no Constraint
routes.MapRoute(
    name: "BlogPost",
    url: "blog/posts/{postId}",
    defaults: new
    {
        controller = "Posts",
        action = "GetPost",
    }, 
);
This route will call into the PostsController of a hypothetical blog application and retrieve a specific post, based on the unique postId. This route will properly match the following URL:
Unfortunately, if will also match this:
The basic Route constraint allows us to test the value of the {postId} parameter to determine if it is a numeric value. We can re-write our route mapping thus:
Example Route Mapping for Blog Application with Added Constraint
routes.MapRoute(
    name: "BlogPost",
    url: "blog/posts/{postId}",
    defaults: new
    {
        controller = "Posts",
        action = "GetPost",
    }, 
    new {postId = @"\d+" }
);
The tiny Regular Expression @"\d+ in the code above basically limits the matches for this route to URLs in which the postId parameter contains one or more digits. In other words, nothing but integers, please.
I'm not going to dig too deep into Regular Expressions here. Suffice it to say that Regular Expressions can be used to great effect in developing a complex routing scheme for your application.

A Note and More Resources for Regular Expressions

Regular Expressions are a powerful tool, and also a giant pain in the ass. As Jamie Zawinskie said, "Some people, when confronted with a problem, think 'I know, I'll use regular expressions.' Now they have two problems." However, Regular Expressions are sometimes the very best tool for the job – restricting route matches in an MVC application is one of those cases. I find three tools most helpful when writing more complex regular expressions. In order of importance:

Custom Route Constraints using IRouteConstraint

The other option for using route constraints is to create a class which implements the IRouteConstraintinterface.
We'll take a quick look at a custom route constraint which can be used to make sure a particular controller is excluded as a match for our default MVC route.
Note: The following code was adapted from a CodeProject article by Vijaya Anand (CodeProject memberAfter2050), originally posted at his personal blog Proud Parrot.
In creating a custom constraint, we first create a new class which implements IRouteConstraint.IRouteConstraint defines a single method, Match for which we need to provide the implementation. In order for the constraint to work, we will also need to create a constructor with which we set the argument(s) required for the method:
The Custom Constraint Class ExcludeController
public class ExcludeController : IRouteConstraint
{
    private readonly string _controller;
    public ExcludeController(string controller)
    {
        _controller = controller;
    }
    public bool Match(HttpContextBase httpContext,
        Route route, string parameterName,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        // Does the _controller argument match the controller value in the route
        // dictionary for the current request?
        return string.Equals(values["controller"].ToString(),
            _controller, StringComparison.OrdinalIgnoreCase);
    }
} 
Now, suppose we have a hypothetical controller ConfigurationController for which we have defined a special route which requires authentication. We don't want the MVC default route map to inadvertently allow access to the Configuration controller for unauthenticated users. We can make sure of this by modifying our default MVC route like so:
Adding a Custom Constraint to the Default MVC Route
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new
    {
        controller = "Home",
        action = "Index",
        id = UrlParameter.Optional
    },
    constraints: new { controller = new ExcludeController("Configuration")}
);
Now, if a URL which has not been matched by any previous route mapping is evaluated for a match with our default route, the MVC framework will call the Match method of our custom constraint ExcludeController. In the case above, if the URL contains the {controller} parameter with a value of "Configuration" the Matchmethod will return a value of false, causing the route to reject the URL as a match.
Stephen Walther presents an excellent tutorial on Custom Route Constraints, and specifically, creating a authentication constraint at his blog in ASP.NET MVC Tip #30 – Create Custom Route Constraints.
11/26/2013 – UPDATE: Since this article was written, ASP.NET MVC 5 has been released. In addition to including Attribute Routing out-of-the-box (as mentioned previously), ASP.NET MVC 5 also brings a new Identity management system to replace the Forms Membership used until now. Check out Extending Identity Accountsand Implementing Role-Based Identity Management using the new Identity libraries in this exciting new release.

ASP.NET MVC Routing Overview (C#)


In this tutorial, you are introduced to an important feature of every ASP.NET MVC application called ASP.NET Routing. The ASP.NET Routing module is responsible for mapping incoming browser requests to particular MVC controller actions. By the end of this tutorial, you will understand how the standard route table maps requests to controller actions.

Using the Default Route Table

When you create a new ASP.NET MVC application, the application is already configured to use ASP.NET Routing. ASP.NET Routing is setup in two places.
First, ASP.NET Routing is enabled in your application's Web configuration file (Web.config file). There are four sections in the configuration file that are relevant to routing: the system.web.httpModules section, the system.web.httpHandlers section, the system.webserver.modules section, and the system.webserver.handlers section. Be careful not to delete these sections because without these sections routing will no longer work.
Second, and more importantly, a route table is created in the application's Global.asax file. The Global.asax file is a special file that contains event handlers for ASP.NET application lifecycle events. The route table is created during the Application Start event.
The file in Listing 1 contains the default Global.asax file for an ASP.NET MVC application.
Listing 1 - Global.asax.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplication1
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}
When an MVC application first starts, the Application_Start() method is called. This method, in turn, calls the RegisterRoutes() method. The RegisterRoutes() method creates the route table.
The default route table contains a single route (named Default). The Default route maps the first segment of a URL to a controller name, the second segment of a URL to a controller action, and the third segment to a parameter namedid.
Imagine that you enter the following URL into your web browser's address bar:
/Home/Index/3
The Default route maps this URL to the following parameters:
    controller = Home
    action = Index
    id = 3
When you request the URL /Home/Index/3, the following code is executed:
HomeController.Index(3)
The Default route includes defaults for all three parameters. If you don't supply a controller, then the controller parameter defaults to the value Home. If you don't supply an action, the action parameter defaults to the valueIndex. Finally, if you don't supply an id, the id parameter defaults to an empty string.
Let's look at a few examples of how the Default route maps URLs to controller actions. Imagine that you enter the following URL into your browser address bar:
/Home
Because of the Default route parameter defaults, entering this URL will cause the Index() method of the HomeController class in Listing 2 to be called.
Listing 2 - HomeController.cs
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index(string id)
        {
            return View();
        }
    }
}
In Listing 2, the HomeController class includes a method named Index() that accepts a single parameter named Id. The URL /Home causes the Index() method to be called with an empty string as the value of the Id parameter.
Because of the way that the MVC framework invokes controller actions, the URL /Home also matches the Index() method of the HomeController class in Listing 3.
Listing 3 - HomeController.cs (Index action with no parameter)
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
}
The Index() method in Listing 3 does not accept any parameters. The URL /Home will cause this Index() method to be called. The URL /Home/Index/3 also invokes this method (the Id is ignored).
The URL /Home also matches the Index() method of the HomeController class in Listing 4.
Listing 4 - HomeController.cs (Index action with nullable parameter)
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index(int? id)
        {
            return View();
        }
    }
}
In Listing 4, the Index() method has one Integer parameter. Because the parameter is a nullable parameter (can have the value Null), the Index() can be called without raising an error.
Finally, invoking the Index() method in Listing 5 with the URL /Home causes an exception since the Id parameter is not a nullable parameter. If you attempt to invoke the Index() method then you get the error displayed in Figure 1.
Listing 5 - HomeController.cs (Index action with Id parameter)
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index(int id)
        {
            return View();
        }
    }
}

Invoking a controller action that expects a parameter value
Figure 01: Invoking a controller action that expects a parameter value (Click to view full-size image)

The URL /Home/Index/3, on the other hand, works just fine with the Index controller action in Listing 5. The request /Home/Index/3 causes the Index() method to be called with an Id parameter that has the value 3.

No comments:

Post a Comment