.Net Core MVC Http Request Routing
By: Tom Chizek
Tags: Model View Controller, C Sharp Programming, Dotnet Core, Dotnet Core Configuration, How To

Introduction

As usual, with these articles, I have a simple reason for writing this article. Over the last few days of working on a project, I have found bugs that were related entirely to misrouted HTTP requests due to mistakes I made in either my configuration or the attributes on my handlers. So let’s talk about the basics of using the .Net Core Http routing configuration and attribute hints. Maybe some of what I am fighting with and correcting in my code will save you some frustration.

First, a quick overview of the topic, every .Net Core MVC web application consists of three major parts. Just as it says in the name, Model — where data is stored, View — where information is displayed, and Controller — where data is processed and manipulated. There can be some overlap in these where the Model knows more about how to handle parts of its internals, so it exposes methods or properties to let the Controller manipulate bits that the Controller shouldn’t be accessing. But for the most part, it is safe to think in the three major blocks. A more complex program will have additional components that the Controllers use to do truly complex work or an additional backend server that the Controller passes the data off to for full processing. But for this article, I am discussing a basic MVC application that is all contained in a single web project, with a set of models, views, and controllers that fit together to make a coherent whole.

How do they fit together? Well, the Model is easy. The other two components get models or ways to find models as input data. So, a view will get the Model it is working on as input as part of the definition, and a controller method will get either a model key value that lets it look the correct Model up and load it for processing, or the data the makes up the Model. Either way, the Model is the data that lets the View and the Controller communicate. What is that communication? Well, for the average application, it is HTTP traffic, usually either in the form of request messages from the View to the Controller or a mix of HTTP page/view/request to the browser showing the views that make up the UI.

Which brings us to the question of how the underlying software determines the handler for any incoming Html request? And decide to send that specific HTTP message to for it to get to the correct method or browser window? The full answer is complicated enough that for a long time, this was the most challenging part of network programming. There are Doctoral Dissertations still being written in how to do this in the least time-consuming or expensive way possible. Fortunately, .Net Core has a set of libraries that hide all that complexity from us. I say fortunately because back in the dark ages, I had to do this by hand multiple times, and let me assure you that writing network routing code is not a fun or trivial exercise. Trust your tools; don’t reinvent the wheel. The odds are if you try to do so, you will take longer and get something worse.

Basics of Configuring Routing

For the most basic MVC applications, the simple routing scheme that is created by the code generation wizard if you pick the “give me a .Net Core MVC application” path when you make your project works just fine. It adds a default route pattern to the application in the Startup class. This default route looks like :

app.UseEndpoints(endpoints =>

{

endpoints.MapControllerRoute(

name: "default",

pattern: "{controller=Home}/{action=Index}/{id?}");

});

Breaking down the routing defined above into its parts, first app.UseEndpoints defines the set of route information that the application will tell the operating system it needs — specifically the network subsystem of the operating system. It takes a builder method that creates a set of route definitions that it can return. Which is what “endpoints” are defining. Here you are writing an inline method that will build that list by building “endpoints.MapControllerRoute” instances. Which while it can be more complex for the most part, we can use the same constructor used by the code generator. This version takes a route name and the pattern which defines what the routed HTTP address looks like matching it to a specific method in your Controller. Specifically, the above pattern “{controller=Home}/{action=Index}/{id?}” says to Route a blank call to your URI to the Home.Index method. That is something like https://myrandom.com would route the same as https://myrandom.com/Home/Index. This simple scheme works in general for simple routings, so a single controller with Create, Edit, Delete, and Index endpoints will work just fine. However, if you do something wild like have data that requires more than a single page to view to keep from taking, oh say, theoretically ten minutes to load the first page. Then things start getting interesting fairly quickly.

Surprise, this is exactly the situation I ran into, so I ended up adding several other routes to my endpoint list. So my Route now looked like:

app.UseEndpoints(endpoints =>

{

endpoints.MapControllerRoute(

name: "default",

pattern: "{controller=Home}/{action=Index}/{id?}");

endpoints.MapControllerRoute(

name: "deeper",

pattern: "{controller}/{action}/{subaction}/{id?}");

endpoints.MapControllerRoute(

name: "paging",

pattern: "{controller}/{action}/{id} page={page}");

endpoints.MapControllerRoute(

name: "paging-deeper",

pattern: "{controller}/{action}/{subaction}/{id} page={page}");

});

I added additional routes without defaults to allow both sub-actions, which would look like https://myrandom.com/Home/Index/SomeRandom/5, which allows for related but different commands like Getting the index of “SomeRandom number 5” and having it call the method Home.SomeRandom with a parameter of five. To do that, you need some additional information in the HomeController you would need to have something like the following:

[HttpGet]

[Route("Index/SomeRandom/{id?}")]

public async Task<IActionResult> SomeRandom(int? id)

{...}

This attribute is the other half of the routing. It tells the receiving network code on the server where to route those incoming requests that we have told the OS we want to receive. These attributes hide a large complex set of code underneath that connects SomeRandom as the callback for an HTTP Get message coming in with the path “Index/SomeRandom,” and any number after the last slash gets parsed and passed into your method as an int, or if none it sends in a null.

These routing patterns are still a relatively simple set of routes, except for the cases where you need a sub-action, you wouldn’t need to worry about telling the routing code underlying .Net Core how to route incoming messages.

Then you add several — in my case, seven — more controller/view sets. The primary Index responses work correctly, but as soon as I called anything that took an id or handled anything except an HTTP Get, I started getting routing error messages on the client-side. Essentially listing off all of the possible matches that were even close with “These all kind of match and we can’t figure it out” but in computer errorish language rather than quite as quippy.

Digging into things, I found that adding full routing information to each Controller solved the problem. What do I mean by full routing information? Well, in the declaration of each Controller, I added a route attribute to define the Route to the Controller. So my controller declarations look like this now:

[Route("Home")]

public class HomeController : Controller

{...} [Route("StarCharts")]

public class StarChartsController : Controller

{...} [Route("Stars")]

public class StarsController : Controller

{...} [Route("Planets")]

public class PlanetsController : Controller

{...}

And so on for all of the controllers. This configuration sets up how the various routes are going to treat the controllers. Then for each method, I put in routing information where it was needed — and it turned out that I needed it in quite a few places.

I’ll take the Atmospheres Controller because it is representative of what I did for all of the controller and view pairs. Also, I am going to truncate most of these code blocks to the parts that are related to routing. While there is quite a bit of interesting activity happening beyond the routing covering everything will dilute this article and its purpose. I may mention some useful bits in passing but will not show entire methods or views to keep this down to a reasonable length.

First, the Controller, here I get the full list of atmosphere objects from the database context, set which page we are on, then use a NuGet Package called X Paged List to build up the current page of atmosphere objects and save it and the current page to the View Bag for use in the View. Then I give the index View the full list of atmospheres that it is expecting.

[HttpGet]

[Route("Index")]

[Route("Index/{id?}")]

public async Task<IActionResult> Index(int? page)

{

_logger.LogInformation($"{_className}.Index(int? {page}) Starting");

var atmospheres = await _context.Atmospheres

.ToListAsync();

_logger.LogInformation($"{_className}.Index(int? {page}) Got stars");

int pageNum = page ?? 1;

IPagedList<Atmosphere> pageOfAtmosphere = atmospheres.ToPagedList<Atmosphere>(pageNum, GeneralCalculations.PageLength);

ViewBag.PageOfAtmospheres = pageOfAtmosphere;

ViewBag.CurrentPage = pageNum;

_logger.LogInformation($"{_className}.Index(int? {page}) Got page now view");

return View(atmospheres);

}

Then the View, the first half is just basic Razor/Html with a touch of CSS thrown in for spice. The most interesting bit is the two uses of View Bag when I can building the title so I can show the current and max pages to expect.

The next part of the View is more interesting. This section is where I use the paged list that was saved in the View Bag to show the current page of atmospheres along with Edit, Details, and Delete buttons. Then at the bottom, below the table, use the PagedListPager extension to Html to show a paging control with the action of paging set to route back to Index with the page number selected.

This paging is less of an issue with Atmospheres with the current state of the database but Stars? There are over 45,000 stars, which comes out of more than 900 pages. Before I added paging and database optimizations, it was taking ten minutes to load the page and several minutes to scroll down. I will write another article on how I optimized the database sometime soon.

The next bit is the Details controller and View. The details of the Details controller is basic and dull. The Controller looks up the id passed in and sends it to the View. The View displays the values. The useful thing to note is the routing information, where the attributes set up what type of Http message and path this method is handling.

[HttpGet]

[Route("Details")]

[Route("Details/{id?}")]

public async Task<IActionResult> Details(Guid? id)

{

…

}

The View is just basic Razor/Html with nothing new or different in it.

The Create handler has just a bit more of interest. It has an Http Get handler that is dead simple,

[HttpGet]

[Route("Create")]

public IActionResult Create()

{

_logger.LogInformation($"{_className}.Create()");

return View();

}

The routing attributes are the only interesting things, and they are there to distinguish this handler from all of the other Http Get handlers that exist both in this class and other classes.

Create also has an Http Post handler to deal with the newly created Atmosphere object. Validating that it is from the source we are expecting, validating that it is the Model we are expecting, then setting values that the user is not allowed to set — and finally, saving the data to the database. I am going to show the entire handler because it has interesting routing and security attributes, as well as some bits in the method itself that I want to discuss briefly. First, the routing, it has the Http Post, and Route attributes telling the system what this method is handling, which should be getting familiar by now. Then there is [ValidateAntiForgeryToken]. This attribute again hides a whole boatload of complexity that .Net Core has automated for us. The attribute tells the system to generate a security code for validating the connection between the sender and the handler. Confirming the sender as expected, and nothing has slipped into the middle modifying values in the transmitted packet — a horrible oversimplification but, in general, terms what is going on. Having to do this by hand even five or six years ago, let alone twenty years ago, was one reason that web applications were so notorious for being insecure. Writing even this baseline level of security by hand was a major undertaking that few companies were willing to spend time, money, or effort to have. Having these tools available is a wonderful shortcut. Don’t write your own when there are tools that do the work.

[HttpPost]

[ValidateAntiForgeryToken]

[Route("Create")]

public async Task<IActionResult> Create([Bind("Id,AtmospherePrimary,AtmosphereComposition,Name,Created,LastUpdated")] Atmosphere atmosphere)

{

_logger.LogInformation($"{_className}.Create([Bind] {atmosphere.ToShortString()})");

if (ModelState.IsValid)

{

_logger.LogInformation($"{_className}.Create([Bind] {atmosphere.ToShortString()}) Valid Model");

atmosphere.Id = Guid.NewGuid();

atmosphere.Created = DateTime.Now;

atmosphere.LastUpdated = DateTime.Now;

_context.Add(atmosphere);

await _context.SaveChangesAsync();

_logger.LogInformation($"{_className}.Create([Bind] {atmosphere.ToShortString()}) Go back to index");

return RedirectToAction(nameof(Index));

}

_logger.LogInformation($"{_className}.Create([Bind] {atmosphere.ToShortString()}) Invalid Model try again");

return View(atmosphere);

}

While some interesting things are going on in the View, there is nothing related to routing. From here, it is a matter of setting the route attributes for each method appropriately. So methods that send data to the client are 99% Http Get handlers. While methods that receive data from the client are 80% Http Post handlers. The reason for the difference is that there is another Http request message that sends data, that is the Http Put message, this is supposed to be the Update a record request. Many applications overload Post with this functionality and ignore Put messages at all. However, you should be aware that you may need a handler for this type of message as well.

Conclusion

Using the .Net Core Http Routing system isn’t difficult. However, it does add a layer of complexity that you need to think about when writing MVC applications. This level of complexity is one of the reasons that some people are moving away from MVC applications to Page Model applications where the controller code is directly in the code behind for each page, and a page may handle multiple operations. Because my preference is still for the model view controller style of application, I live with the slight increase in design thought that I need to apply for the Controller’s routing needs. But if you don’t like this, then it may be that you would be more comfortable investigating the page model style. Either way, they are both attempting to do the same thing, provide a set of tools to let you get your vision onto a website as quickly and easily as possible.

Good luck, and have fun.