What I Learned Building with ASP.NET Core: Part 1 - Routing


Spaghetti-Junction-CropI’ve been digging into ASP.NET Core for quite a while now (from the early betas through the current release). Recently I re-wrote the Atlanta Code Camp website using ASP.NET Core.

Through that process I’ve learned some new lessons about ASP.NET Core and this series of blog posts is going to talk about those lessons. I have no idea how many parts it will have, but I’ll post all that I’ve learned in building a site with real users ; )

Originally, the old Atlanta Code Camp utilized MVC Areas to handle the different years that we held the code camp. I wanted to keep that functionality (and even move prior years to the new site) but I wanted to avoid Areas. I like areas for real site areas, but it felt like a hack to use it for separate years. In addition, the Areas forced me to copy/paste each year and modify them every year.

I’ve just learned about Feature folders in MVC 6 and wish I knew about them as that might be a way I would have segmented the project. If you haven’t seen anything about feature slices, please go read Steve Smith’s great article on it but I’m not going to explain or use them in this post.

Instead, I decided to just use routing. In MVC6, you have two kinds of routes:

  • Global Routes
  • Attributed Routes

I find that creating some global routes are useful to cover the main URL patterns. Then adding routes via controller and action attributes for cases where the global routes were too coarse grained. For example, a global route for the {controller}/{action} pattern makes sense, but then adding attributed routes for associations like {controller}/{action}/{subobject}.

For example, I created two global routes for the project:

routes.MapRoute(
  name: "Events",
  template: string.Concat("{moniker}/{controller=Root}/{action=Index}/{id?}")
  );

routes.MapRoute(
  name: "Default",
  template: "{controller=Root}/{action=Index}/{id?}"
  );

In the first route, I’m using a moniker to decide what year the code camp we’re using (I’m using the phrase ‘moniker’ to mean something that describes the separation, for me it’s always years). This works great..mostly.

In my controllers, the moniker becomes a required parameter so I use it in every action. For example:

public IActionResult Index(string moniker)
{
  var sponsors = _repo.GetSponsors(moniker);
  return View(sponsors);
}

My issue was that there were attributes routes that were cherry picking my calls if they were four parts. I thought that the global route would take precedence, but not quite. What the routing system does by default is take your global routes and then inject a container that will hold all the attributes routes as the first member of the routes (see the repo for the exact code that does this). This means my routing table ended up looking like this:

image

When the route table is finally configured, it ends up looking through all the routes in attributed routes before any of your global routes. So the order of routes when it’s evaluated looks like this:

image

My moniker route failed to be hit because one of the attributed routes was incorrectly matching before it. I haven’t found a perfect way to debug routes yet in ASP.NET Core, but I was able to look at the routes collection to see what was happening. The trick it seems is that you can specify an order in the Attributed routes. So I found the particular route that was colliding and set it’s order to 1. The way the routing order works is that it uses the collection order, but allows you to also specify levels of the ordering through numbers. By default all routes have an order of zero. By including 1 as the order of one of the attributed routes, I was able to push it to be ordered later. For example:

image

The problem with this is that ASP.NET Core separates the idea of routing and MVC Routing into two different repositories. Attributed routes are part of MVC, not the underlying Routing framework which MVC relies on. In this way, the idea of a route order only exists in the Attributed routes as seen in the repo. This works because MVC 6 creates it’s own router to determine which of the routes matches including order, constraints, and the URL pattern.

I am still not sure by reading the code whether ordered attributed routes are only ordered within the attributed routes or the global routes too. In my case, the real problem was that my global route including the moniker I wanted to be tested before most attributed routes, but I wanted my catch all last global route to be last in every case. Without digging deep into providing my own routing table or router, this seems impossible. I’m hoping that someone on the team sees this and can help us understand it.

Ultimately I chickened out and just created an attributed route on the Root controller to avoid the collision. I don’t feel great about the solution, but sometimes pragmatism wins out. I did learn a lot about how MVC routing works through the pain.

Has your experience been different than mine?




Application Name WilderBlog Environment Name Production
Application Ver 1.1.0.0 Runtime Framework .NETCoreApp,Version=v1.1
App Path D:\home\site\wwwroot Runtime Version .NET Core 4.6.25211.01
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86