Routing and URL handling in MVC

   —   

The process of URL resolution in MVC is fundamentally different from Portal Engine. It is a vast topic, so nobody really wants to learn it all. However, to set up robust yet simple routing that will make your editors happy, you just need to know a few basics. Are you ready?

In this article, I'll explain the concepts and examples of MVC routing in natural order; from the first incoming request, all the way down to the point where content is served back.

But first things first. To make sure your MVC website integrates well with Kentico, follow the steps in our documentation that ensure you get the right packages and code.

Routing

Unlike Web Forms, MVC makes indirect relations between URLs of incoming requests to physical code files that handle them. In pure Web Forms (without rewriting in place), each URL should point to some .aspx file that sits on the disk:

https://www.domain.com/some/physical/file.aspx?someParameter=parameterValue

In MVC, we have a controller classes with action methods. These take parameters from the URL. The infrastructure that guides each request to the right controller is called routing. You write those guiding rules as routes that together form a routing table. Each variation of a route is ultimately bound to a controller action method. This allows for SEO-friendly URLs such as this:

https://www.domain.com/blog/on-roasts

Defining Routes

Routes can be defined in a central place (in ~/App_Start/RouteConfig.cs), in controller actions (attribute routes), or in a combination of the two. There's not much difference between them, it only depends on which method suits you best. In the RouteConfig class, you define a route as follows:

routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );

Notice the URL argument in the middle. This is the URL pattern that each request is being compared against at runtime. In this example, the controller and action URL segments represent well-known identifiers that MVC resolves to controller class and action method names. Apart from the pattern, each route can have default values should an incoming request omit to mention them.

You can also make the last (trailing) URL segment optional, such as ID in the mentioned example.

The above code snippet is actually being inserted automatically into each MVC 5 project - it is the default conventional route. But besides that, you can add any number of your custom additional routes. Remember, the routes need to be distinguishable by the URL argument to make sure the right route can be selected at runtime.

At runtime, routes are evaluated in the same order in which they were defined in RouteConfig, from top to bottom.

In the example on the right side, the first route would be matched for all requested URLs.

Route Constraints

While routes have URL patterns to grab an incoming request, they may have route constraints to release that request for subsequent routes to evaluate it.

routes.MapRoute(null, "{category}/{product}", new { controller = "Shop", action = "ShowProduct" }, new { category = "coffee|brewers|accessories"} );

You can find a number of built-in route constraints in the System.Web.Mvc.Routing.Constraints namespace. You can write your own constraints that may, for example, compare the request against the Kentico database.

Controllers and Actions

Now that you know about URL pattern matching, let's take a look at controllers and actions. That is the final step of routing a URL request. When a request reaches a controller action, that action is responsible for generating the output HTML and responding with it.

Model Binding

The specific URL segments you defined for all routes will be paired with your controller action method's parameters. This is called model binding and works for querystring params as well as data in request body. Consider the following route that points to ShopController and Index action:

shop/{category}/{product}

The action signature will look like this:

ActionResult Index(string category, string product)

Both parameters will be automatically bound by model binding. The data types of the parameters need to be the same as the URL segments in the route. For example, if the action parameter requires an integer, it won't be matched with a string data from the URL segment and you will see an error.

On a side note, not only URL segments are bound to action method parameters but also, POST data submitted through forms are bound this way.

There is a DefaultControllerFactory class that is responsible for inspecting your code for controllers and supplying a proper controller instance for further processing. The factory first decides in which namespaces to look. If you specified a Namespaces property in the defaults argument of your route, it will narrow the search accordingly. Then, it will pick the right type of your controller and it will produce an instance of it using reflection.

All right. So this is how the request is processed:

  • The request travels to Routing table
  • It successfully passes route constraint checks
  • The controller factory creates BlogController instance
  • The Posts action method is invoked

Examples of Common Routes

Now that we've covered the whole routing and its components, let's take a look at some examples of commonly used routes on Kentico sites.

routes.MapRoute( "Based on nodeAlias", "{controller}/{nodeGuid}/{nodeAlias}", new { action = "Index", nodeAlias = UrlParameter.Optional }, new { type = "article|coffee" } );

URL examples Note
/article/e7da5ff7-a0d2-4a8f-bf36-75bb0b4dc964/on-roasts
/article/.../coffee-/processing-techniques
/coffee/.../coffee-sampler
Allows routing based on type of content. The examples use ArticleController and CoffeeController. The URL route contains constraint for type. If it does not match these controllers, the route will not be used.

routes.MapRoute( "Special pages", "login", new { controller = "Login", action = "Index" } );

URL examples Note
/login
...
/search
/not-found
Routes for special pages.

routes.MapRoute( "RSS", "rss/{tagName}", new { controller = "Rss", action = "Index", tagName = UrlParameter.Optional } );

URL examples Note
/rss/coffee Routes a request for XML content based on a tagName keyword.

routes.MapRoute( "FormSubmission", "form/{formId}"`, new { controller = "Form", action = "Submit" }, new { formId = @"\d+" } );

URL examples Note
/form/5 Example of route that handles form submissions. The route also has a constraint on the formId parameter - it must be a number.

routes.MapRoute( "Culture based", "{cultureCode}/{nodeAlias}", new { controller = "Content", action = "Index" } );

URL examples Note
/en-US/on-roasts
/cs-CZ/coffee-processing-techniques
Enables culture-code based routing to a specific controller handling content.
Note: Requires additional custom logic to work reliably.

routes.MapRoute( "Fallback", "{*nodeAliasPath}", new { controller = "Fallback", action = "Index" } );

URL examples Note
/contact-us /article/on-roasts Enables completely free routing, matches any URL. This route can be used to handle routing in a completely custom way or as a fallback for traditional routes.
Note: Requires additional custom logic to work properly.

The Best Way to MVC Routing

You see there is a lot of flexibility when defining MVC routes, but there is no one best way. You always need to specify your own policy for URLs; how a specific type of content should be accessed on your site. Having the content grouped in specific sections dramatically helps with maintainability (easy to debug) and SEO while editors can still manage the content-specific aliases.

If you wish to learn more about routes, navigate to ASP.NET MVC Routing Overview in Microsoft docs.

What is your experience with MVC routes? Tell us in comments below.

Share this article on   LinkedIn

Jan Lenoch

I'm a developer trainer at Kentico. I create hands-on developer trainings of ASP.NET MVC and Kentico. I listen to the voice of our customers and I improve our training courses on a continuous basis. I also give feedback to the internal teams about developers' experience with Kentico.