Professional Documents
Culture Documents
P PPP P P MP PP PP P P PP PPP P P P
P PPP P P MP PP PP P P PP PPP P P P
Once the above class was added to our application, the ASP.NET MVC Framework automatically handled routing the incoming URLs to the appropriate action method on our controller to process. In today's blog post we are going to drill into exactly how this URL mapping happened, as well as explore more advanced routing scenarios we can take advantage of with the ASP.NET MVC Framework. I'll also demonstrate how you can easily unit test URL routing scenarios.
2. Construct outgoing URLs that can be used to call back to Controllers/Actions (for example: form posts, <a href=""> links, and AJAX calls) Having the ability to use URL mapping rules to handle both incoming and outgoing URL scenarios adds a lot of flexibility to application code. It means that if we want to later change the URL structure of our application (for example: rename /Products to /Catalog), we could do so by modifying one set of mapping rules at the application level - and not require changing any code within our Controllers or View templates.
The ASP.NET Application class enables developers to handle application startup/shutdown and global error handling logic. The default ASP.NET MVC Project Template automatically adds an Application_Start method to the class and registers two URL Routing rules with it:
The first routing rule above indicates that the ASP.NET MVC framework should by default map URLs to Controllers using a "[controller]/[action]/[id]" format when determining which Controller class to instantiate, and which Action method to invoke (along with which parameters should be passed in). This default routing rule is why a URL request for /Products/Detail/3 in our e-commerce browsing sample from Part 1 automatically invokes the Detail method on our ProductsController class and passes in 3 as the id method argument value:
The second routing rule above is added to special-case the root "Default.aspx" URL in our application (which is sometimes passed by web servers in place of "/" when handling requests for the root URL of an application). This rule ensures that requests for either the root "/Default.aspx" or "/" to our application are handled by the "Index()" action on the "HomeController" class (which is a controller automatically added by Visual Studio when we created a new application using the "ASP.NET MVC Web Application" project template).
Or by taking advantage of the new object initializer feature in the VS 2008 C# and VB compilers to set the properties more tersely:
The "Url" property on the Route class defines the Url matching rule that should be used to evaluate if a route rule applies to a particular incoming request. It also defines how the URL should be tokenized for parameters. Replaceable parameters within the URL are defined using a [ParamName] syntax. As we'll see later, we aren't restricted to a fixed set of "well known" parameter names - you can have any number of arbitrary parameters you want within the URL. For example, I could use a Url rule of "/Blogs/[Username]/Archive/[Year]/[Month]/[Day]/[Title]" to tokenize incoming URLs to blog posts - and have the MVC framework automatically parse and pass UserName, Year, Month, Day and Title parameters to my Controller's action method. The "Defaults" property on the Route class defines a dictionary of default values to use in the event that the incoming URL doesn't include one of the parameter values specified. For example, in the above URL mapping examples we are defining two default URL parameter values - one for "[action]" and one for "[id]". This means that if a URL for /Products/ is received by the application, the routing system will by default use "Index" as the name of the action on the ProductsController to execute. Likewise, if /Products/List/ was specified, then a null string value would be used for the "ID" parameter. The "RouteHandler" property on the Route class defines the IRouteHandler instance that should be used to process the request after the URL is tokenized and the appropriate routing rule to use is determined. In the above examples we are indicating that we want to use the System.Web.Mvc.MvcRounteHandler class to process the URLs we have configured. The reason for this extra step is that we want to make sure that the URL routing system can be used for both MVC and non-MVC requests. Having this IRouteHandler interface means we will be able to cleanly use it for non-MVC requests as well (such as standard WebForms, Astoria REST support, etc). There is also a "Validation" property on the Route class that we'll look at a little later in this post. This property allows us to specify pre-conditions that need to be met for a particular routing rule to match. For
example, we could indicate that a routing rule should only apply for a specific HTTP verb (allowing us to easily map REST commands), or we could use a regular expression on arguments to filter whether a routing rule should match. Note: In the first public MVC preview the Route class isn't extensible (instead it is a data class). For the next preview release we are looking to make it extensible and enable developers to add scenario specific route classes (for example: a RestRoute sub-class) to cleanly add additional semantics and functionality.
We'll then define two Action methods within our SearchController class. The Index() action method will be used to present a search page that has a TextBox for a user to enter and submit a search term. The
Results() action will be used to handle the form submission from it, perform the search against our database, and then display the results back to the user:
Using the default /[controller]/[action]/[id] URL route mapping rule, we could "out of the box" use URLs like below to invoke our SearchController actions:
Note that the reason the root /Search URL by default maps to the Index() action method is because the /[controller]/[action]/[id] route definition added by default when Visual Studio creates a new project sets "Index" as the default action on Controllers (via the "Defaults" property):
While URLs like /Search/Results?query=Beverages are perfectly functional, we might decide we want slightly "prettier" URLs for our search results. Specifically we might want to get rid of the "Results" action
name from the URL, and pass in the search query as part of the URL instead of using a QueryString argument. For example:
We could enable these "prettier" search result URLs by adding two custom URL Route mapping rules before the default /[controller]/[action]/[id] rule like below:
With the first two rules we are now explicitly specifying the Controller and Action parameters for /Search/ URLs. We are indicating that "/Search" should always be handled by the "Index" action on the SearchController. Any URL with a sub-URL hierarchy (/Search/Foo, /Search/Bar, etc) is now always handled by the "Results" action on the SearchController. The second routing rule above indicates that anything beyond the /Search/ prefix should be treated as a parameter called "[query]" that will then be passed as a method parameter to our Results action method on SearchController:
Most likely we will also want to enable paginated search results (where we only show 10 search query results at a time). We could do this via a querystring argument (for example: /Search/Beverages?page=2) or we could optionally embed the page index number as part of the URL as well (for example: /Search/Beverages/2). To support this later option all we'd need to-do is add an extra optional parameter to our 2nd routing rule:
Notice above how the new URL rule match is now "Search/[query]/[page]". We've also configured the default page index to be 1 in cases where it is not included in the URL (this is done via the anonymous type passed as the "Defaults" property value). We can then update our SearchController.Results action method to take this page parameter as a method argument:
And with that we now have "pretty URL" searching for our site (all that remains is to implement the search algorithm - which I will leave as an exercise to the reader <g>).
If we pass in a URL like /Products/Detail/12 to our application, the above routing rule will be valid. If we pass in /Products/Detail/abc or /Products/Detail/23232323232 it will not match.
automatically pick up the special Search results route rule we configured earlier in this post, and the "href" attribute they generate automatically reflect this:
In particular note above how the second call to Html.ActionLink automatically mapped the "page" parameter as part of the URL (and note how the first call omitted the page parameter value - since it knew a default value would be provided on the server side). Url.Action In addition to using Html.ActionLink, ASP.NET MVC also has a Url.Action() view helper method. This generates raw string URLs - which you can then use however you want. For example, the code snippet below:
would use the URL routing system to return the below raw URL (not wrapped in a <a href=""> element):
Controller.RedirectToAction ASP.NET MVC also supports a Controller.RedirectToAction() helper method that you can use within controllers to perform redirects (where the URLs are computed using the URL routing system). For example when the below code is invoked within a controller:
It internally generates a call to Response.Redirect("/Search/Beverages") DRY The beauty of all of the above helper methods is that they enable us to avoid having to hard-code in URL paths within our Controller and View Logic. If at a later point we decide to change the search URL route mapping rule back from "/Search/[query]/[page]" to "/Search/Results/[query]/[page]" or /Search/Results?query=[query]&page=[page]" we can easily do so by editing it in one place (our route
registration code). We don't need to change any code within our views or controllers to pick up the new URL (this maintaining the "DRY principle").
Constructing Outgoing URLs from the Routing System (using Lambda Expressions)
The previous URL helper examples took advantage of the new anonymous type support that VB and C# now support with VS 2008. In the examples above we are using anonymous types to effectively pass a sequence of name/value pairs to use to help map the URLs (you can think of this as a cleaner way to generate dictionaries). In addition to passing parameters in a dynamic way using anonymous types, the ASP.NET MVC framework also supports the ability to create action routes using a strongly-typed mechanism that provides compile-time checking and intellisense for the URL helpers. It does this using Generic types and the new VB and C# support for Lambda Expressions. For example, the below anonymous type ActionLink call:
In addition to being slightly terser to write, this second option has the benefit of being type-safe, which means that you get compile-time checking of the expression as well as Visual Studio code intellisense (you can also use refactoring tools with it):
Notice above how we can use intellisense to pick the Action method on the SearchController we want to use - and how the parameters are strongly-typed. The generated URLs are all driven off of the ASP.NET MVC URL Routing system.
You might be wondering - how the heck does this work? If you remember eight months ago when I blogged about Lambda Expressions, I talked about how Lambda expressions could be compiled both as a code delegate, as well as to an expression tree object which can be used at runtime to analyze the Lambda expression. With the Html.ActionLink<T> helper method we using this expression tree option and are analyzing the lambda at runtime to look up the action method it invokes as well as the parameters types, names and values that are being specified in the expression. We can use these with the MVC Url Routing system to return the appropriate URL and associated HTML. Important: When using this Lambda Expression approach we never actually execute the Controller action. For example, the below code does not invoke the "Results" action method on our SearchController:
When this hyperlink is clicked by an end-user it will then send back a http request to the server that will invoke the SearchController's Results action method.
You can then write unit tests that create their own RouteCollection instance and call the Application's RegisterRoutes() helper to register the application's route rules within it. You can then simulate requests to the application and verify that the correct controller and actions are registered for them - without having to worry about any side-effects:
Summary
Hopefully this post provides some more details about how the ASP.NET MVC Routing architecture works, and how you can use it to customize the structure and layout of the URLs you publish within your ASP.NET MVC applications. By default when you create a new ASP.NET MVC Web Application it will pre-define a default /[controller]/[action]/[id] routing rule that you can use (without having to manually configure or enable anything yourself). This should enable you to build many applications without ever having to register your own custom routing rules. But hopefully the above post has demonstrated that if you do want to custom structure your own URL formats it isn't hard to-do - and that the MVC framework provides a lot of power and flexibility in doing this. Hope this helps, Scott