ASP.NET 5 Deep Dive: Routing

In this blog post, I focus on how routing is implemented in ASP.NET 5 and MVC 6. I explain how to create routes, use RESTful routes, and use route constraints.

While researching this blog post, I took full advantage of the open-source code for ASP.NET 5 located at the following GitHub repositories:

https://github.com/aspnet

Routing in Previous Versions of ASP.NET

Previous versions of ASP.NET supported two types of routing: convention-based routing and attribute-based routing. When using convention-based routing, you could define your routes in your application RouteConfig.cs file like this:

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 code above defines a Default route that maps a request like “/products/details” to the ProductsController.Details() action.

ASP.NET MVC 5 introduced support for attribute-based routing. The primary advantage of attribute-based routing is that it allowed you to define your routes in the same file as your controller. For example, here’s an example of creating a ProductsController that includes both a [RoutePrefix] and [Route] attribute used for routing:

[RoutePrefix("Products")]
public class ProductsController : Controller
{
    [Route("Index")]
    public ActionResult Index()
    {
        return View();
    }
}

Regardless of whether you used convention-based routing or attribute-based routing, all of your routes ended up in a static collection located at RouteTable.Routes.

If you used the ASP.NET Web API then routing worked in a similar way. You could define convention-based routes in the WebApiConfig.cs file and add attribute-based routes to your Web API controller classes.

However, there were two important differences between routing in MVC and the Web API. First, the Web API supported RESTful routes by default. If you gave a Web API controller action a name that started with Get, Post, Put, Delete then you could invoke the action by performing an HTTP GET, POST, PUT, or DELETE request. For example, you could invoke a Web API action named PostMovie() simply by performing an HTTP POST.

The other important difference between MVC and the Web API was subtler. Behind the scenes, MVC and the Web API did not share the same routing framework. Routing in MVC and the Web API followed similar patterns, but routing was implemented with different code (originally written by different Microsoft teams).

Everything in ASP.NET 5 and MVC 6 has been rewritten from the ground up. This includes the routing framework used by both MVC and the Web API. There are several important changes to routing that I discuss in this blog post.

First, in ASP.NET 5, there is no longer a distinction between MVC and Web API controllers. Both MVC and Web API controllers derive from the very same Controller base class.

This means that the same routing framework is used for both MVC and the Web API. With ASP.NET 5, anything that is true of routing for MVC is also true for the Web API.

Second, unlike earlier versions of MVC, default routes are created for you automatically. You don’t need to configure anything to route a request to a controller action. A set of default routes is created for you automatically.

Third and finally, in MVC 6, you are provided with a rich set of inline constraints and parameter options that you can use with both convention-based and attributed-based routing. For example, you can constrain the data type of your route parameters, mark a parameter as optional, and provide default values for your parameters right within your route templates.

Creating Routes

Let’s start with the very basics. I’m going to show you the minimal amount of code that you need to use routing in an ASP.NET 5 application. I won’t even assume that you are using MVC.

Here’s my Startup class:

using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;

namespace RoutePlay
{
    public class Startup
    {

        public void Configure(IApplicationBuilder app)
        {
          RouteCollection routes = new RouteCollection();
          routes.Add(new TemplateRoute(new DebuggerRouteHandler("RouteHandlerA"), "test/{a}/{b}", null));
          routes.Add(new TemplateRoute(new DebuggerRouteHandler("RouteHandlerB"), "test2", null));
          app.UseRouter(routes);
        }
    }
}

In the code above, I’ve created a Configure() method that I use to configure routing. I create a Route collection that contains two routes. Finally, I pass the Route collection to the ApplicationBuilder.UseRouter() method to enable the routes.

The routes are created with the TemplateRoute class. The TemplateRoute class is initialized with a Route Handler and a URL template. The first route – named RouteHandlerA — matches URLs with the pattern “test/{a}/{b}” and the second route – named RouteHandlerB – matches URLs with the pattern “test2”.

The essence of routing is mapping an incoming browser request to a Route Handler. In the code above, requests are mapped to a custom handler that I created named DebuggerRouteHandler.

Here’s the code for the DebuggerRouteHandler:

using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing;
using System;
using System.Threading.Tasks;

namespace RoutePlay
{
	public class DebuggerRouteHandler : IRouter
	{
		private string _name;

		public DebuggerRouteHandler(string name)
		{
			_name = name;
		}

		public string GetVirtualPath(VirtualPathContext context)
		{
			throw new NotImplementedException();
		}

		public async Task RouteAsync(RouteContext context)
		{
			var routeValues = string.Join("", context.RouteData.Values);
            var message = String.Format("{0} Values={1} ", _name, routeValues);
			await context.HttpContext.Response.WriteAsync(message);
			context.IsHandled = true;
		}
	}
}

The DebuggerRouteHandler simply displays the name of the route and any route values extracted from the URL template. For example, if you enter the URL “test/apple/orange” into the address bar of your browser then you get the following response:

DebuggerRouteHandler

Creating Routes with MVC 6

When using MVC 6, you don’t create your Route collection yourself. Instead, you let MVC create the route collection for you.

Here’s a Startup class that is configured to use MVC 6:

using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;

namespace RoutePlay
{
	public class Startup
    {
		public void ConfigureServices(IServiceCollection services)
		{
			services.AddMvc();
		}

		public void Configure(IApplicationBuilder app)
        {
			app.UseMvc();
		}
	}
}

The ConfigureServices() method is used to register MVC with the Dependency Injection framework built into ASP.NET 5. The Configure() method is used to register MVC with OWIN.

Here’s what my MVC 6 ProductsController looks like:

using Microsoft.AspNet.Mvc;

namespace RoutePlay.Controllers
{
    public class ProductsController : Controller
    {
        public IActionResult Index()
        {
            return Content("It Works!");
        }
    }
}

Notice that I have not configured any routes. I have not used either convention-based or attribute-based routing, but I don’t need to do this. If I enter the request “/products/index” into my browser address bar then I get the response “It Works!”:

Default Routes

When you call ApplicationBuilder.UseMvc() in the Startup class, the MVC framework adds routes for you automatically. Here’s what the framework code for the UseMvc() method looks like:

public static IApplicationBuilder UseMvc([NotNull] this IApplicationBuilder app)
{
    return app.UseMvc(routes =>
    {
    });
}

public static IApplicationBuilder UseMvc(
    [NotNull] this IApplicationBuilder app,
    [NotNull] Action<IRouteBuilder> configureRoutes)
{
    // Verify if AddMvc was done before calling UseMvc
    // We use the MvcMarkerService to make sure if all the services were added.
    MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);

    var routes = new RouteBuilder
    {
        DefaultHandler = new MvcRouteHandler(),
        ServiceProvider = app.ApplicationServices
    };

    configureRoutes(routes);

    // Adding the attribute route comes after running the user-code because
    // we want to respect any changes to the DefaultHandler.
    routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
        routes.DefaultHandler,
        app.ApplicationServices));

    return app.UseRouter(routes.Build());
}

The AttributeRouting.CreateAttributeMegaRoute() does all of the heavy-lifting here (the word “Mega” in its name is very appropriate). The CreateAttributeMegaRoute() method iterates through all of your MVC controller actions and builds routes for you automatically. Thank you CreateAttributeMegaRoute() — you are going to save me tons of work!

Convention-Based Routing

You can use convention-based routing with ASP.NET MVC 5 by defining the routes in your project’s Startup class. For example, here is how you would map the requests /Super and /Awesome to the ProductsController.Index() action:

using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;

namespace RoutePlay
{
	public class Startup
    {
		public void ConfigureServices(IServiceCollection services)
		{
			services.AddMvc();
		}

		public void Configure(IApplicationBuilder app)
		{
			app.UseMvc(routes =>
			{
				// route1
				routes.MapRoute(
					name: "route1",
					template: "super",
					defaults: new { controller = "Products", action = "Index" }
				);
				// route2
				routes.MapRoute(
					name: "route2",
					template: "awesome",
					defaults: new { controller = "Products", action = "Index" }
				);
			});
        }
	}
}

If you squint your eyes really hard then the code above in the Configure() method for setting up the routes looks similar to the code in a RouteConfig.cs file.

Attribute-Based Routing

You also can use attribute-based routing with MVC 6. Here’s how you can modify the ProductsController Index() action so you can invoke it with the “/Things/All” request:

using Microsoft.AspNet.Mvc;

namespace RoutePlay.Controllers
{
    [Route("Things")]
    public class ProductsController : Controller
    {
        [Route("All")]
        public IActionResult Index()
        {
            return Content("It Works!");
        }
    }
}

Notice that both the ProductsController class and the Index() action are decorated with a [Route] attribute. The combination of the two attributes enables you to invoke the Index() method by requesting “/Things/All”.

Here’s what a Web API controller looks like in MVC 6:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;

namespace RoutePlay.Controllers.Api.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Notice that a Web API controller, just like an MVC controller, derives from the base Controller class. That’s because there is no difference between a Web API controller and MVC controller in MVC 6 – they are the exact same thing.

You also should notice that the Web API controller in the code above is decorated with a [Route] attribute that enables you to invoke the Movies controller with the request “/api/movies”. The special [controller] and [action] tokens are new to MVC 6 and they allow you to easily refer to the controller and action names in your route templates.

If you mix convention-based and attribute-based routing then attribute-based routing wins. Furthermore, both convention-based and attribute-based routing win over the default routes.

Creating Restful Routes

You can use RESTful style routing with both MVC and Web API controllers. Consider the following controller (a mixed MVC and Web API controller):

using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.WebUtilities.Collections;

namespace RoutePlay.Controllers
{
    [Route("[controller]")]
    public class MyController : Controller
    {
		// GET: /my/show
		[HttpGet("Show")]
		public IActionResult Show()
		{
			return View();
		}

		// GET: /my
		[HttpGet]
		public IActionResult Get()
		{
			return Content("Get Invoked");
		}

		// POST: /my
		[HttpPost]
		public IActionResult Post()
		{
			return Content("Post Invoked");
		}

		// POST: /my/stuff
		[HttpPost("Stuff")]
		public IActionResult Post([FromBody]string firstName)
		{
			return Content("Post Stuff Invoked");
		}


	}
}

The MyController has the following four actions:

  • Show() – Invoked with an HTTP GET request for “/my/show”
  • Get() – Invoked with an HTTP GET request for “/my”
  • Post() – Invoked with an HTTP POST request for “/my”
  • Post([fromBody]string firstName) – Invoked with an HTTP POST request for “/my/stuff”

Notice that the controller is decorated with a [Route(“[controller]”)] attribute and each action is decorated with a [HttpGet] or [HttpPost] attribute.

If you just use the [HttpPost] attribute on an action then you can invoke the action without using the action name (for example, “/my”). If you use an attribute such as [HttpPost(“stuff”)] that includes a template parameter then you must include the action name when invoking the action (for example, “/my/stuff”).

The Show() action returns the following view:

<p>
	<a href="/my">GET</a>
</p>
<form method="post" action="/my">
	<input type="submit" value="POST" />
</form>
<form method="post" action="/my/stuff">
	<input name="firstname" />
	<input type="submit" value="POST STUFF" />
</form>

If you click the GET link then the Get() action is invoked. If you post the first form then the first Post() action is invoked and if you post the second form then the second Post() action is invoked.

RESTful

Creating Route Constraints

You can use constraints to constrain the types of values that can be passed to a controller action. For example, if you have a controller action that displays product details for a particular product given a particular product id, then you might want to constrain the product id to be an integer:

using Microsoft.AspNet.Mvc;

namespace RoutePlay.Controllers
{
    [Route("[controller]")]
    public class ProductsController : Controller
    {

		[HttpGet("details/{id:int}")]
		public IActionResult Details(int id)
		{
			return View();
		}

	}
}

Notice that the Details() action is decorated with an [HttpGet] attribute that uses the template “details/{0:int}” to constrain the id passed to the action to be an integer. There are plenty of other inline constraints that you use such as alpha, minlength, and regex.

ASP.NET 5 introduces support for optional parameters. You can use a question mark ? in a template to mark a parameter as optional like this:

[HttpGet("details/{id:int?}")]
public IActionResult Details(int id)
{
	return View();
}

You can invoke the Details() action in the controller above using a URL like “/products/details/8” or a URL that leaves out the id like “/products/details” (in that case, id has the value 0).

Finally, ASP.NET 5 introduces support for default parameter values. If you don’t supply a value for the id parameter then the parameter gets the value 99 automatically:

[HttpGet("details/{id:int=99}")]
public IActionResult Details(int id)
{
	return View();
}

Summary

ASP.NET 5 (and ASP.NET MVC 6) includes a new routing framework rewritten from the ground up. In this blog post, I’ve provided a deep dive into how this new framework works.

First, I discussed how routing works independently of MVC 6. I explained how you can create a new Route collection in the Startup class.

Next, I demonstrated how you can use MVC 6 default routes, convention-based routes, and attribute routes. I also elaborated on how you can create controllers that follow RESTful conventions.

Finally, I discussed how you can take advantage of inline constraints, optional parameters, and default parameter values.

If you want to dig more deeply into routing then I recommend taking a look at the following two GitHub aspnet repositories:

https://github.com/aspnet/Routing
https://github.com/aspnet/Mvc

Discussion

  1. Andy Roberts says:

    A small thing, but the comments in the second code block in the Attribute Based Routing section are wrong, they should be api/movies not api/values. Hopefully most people would figure it out quickly, but I’m sure you’re going to get a lot of views on this post so always good to get it right 🙂

    PS I’m enjoying your coverage of ASP.NET 5 stuff so far, you seem to be one of the few people covering it in any detail…

  2. Andy Walker says:

    Great set of blogs! Keep them coming.

    I have been able to get the movie app running but have not found a way to debug the non-optimized code. Is there a way to run the application against the code in the scripts folder not the wwwroot files? This is fairly typical behavior in yeoman, or applications you manually scaffold, that use the grunt/gulp infrastructure.

    For example you can either run the non-optimized or optimized code using grunt-contrib-connect task. You can debug either of them in the browser’s debugger. I would like to use the Visual Studio debugger on optimized and non-optimized code if possible.

  3. Jordan says:

    Looks good – MVC and WebAPI being totally different frameworks but being made to look the same can be a bit confusing.

    However, personally I’d seriously question the use of the term “controller” when building HTTP services, RESTful or otherwise.

    A “controller” is a something which is responsible for receiving user input, delegating any work to the appropriate component, and then displaying the correct view. The term shouldn’t be used when building a service.

    ASP.NET isn’t the only framework which makes this mistake, but it is a mistake.