.Net Core Actions, Routes, Handlers, and Methods
By: Tom Chizek
Tags: Dotnet Core, Razor, How To, C Sharp Programming, Software Development

Introduction

I am going to discuss four interrelated terms and the code that they represent in this article. These are terms that sometimes get used interchangeably or applied somewhat haphazardly, describing the same block of code. But these terms have specific meanings, intentions, and uses inside your application. These uses are similar across application types and languages, but I am focusing on C# .Net Core Web Applications. Misapplying them is not inherently wrong, but it indicates that you possibly do not understand what the interaction between these terms is and how to use them in your application.

There are several ways to separate these terms, but in general, Actions and Routes are interface elements that exist purely as the external web interface to your application. Internal to your application, these are not code but attributes and configuration of objects. These provide the communication interface that an external web application uses to communicate with your application.

Handlers are a less defined area; this is because there is nothing that is directly called a “handler” in .Net Core. There are EventHandlers, Callbacks, Delegates, Events, and their Handling. But handler is used as shorthand for this whole group of objects, methods, events, properties, and variables. In general, if you hear or read handler, be on alert because what you are seeing is that someone does not quite know what they are using but know it is about handling something. Usually, this something ends up being either a delegate passed in as a callback or a method used as an event handler. Again, a delegate, but from your code to the system rather than from external application to your code.

Methods are the best defined and best understood. These are the building blocks of a C# application — you write your code into these. If you do not know what a method is, why are you reading this article? But seriously, all the other terms come down to running one or more methods in the end. These methods may be called handlers, actions, callbacks, endpoints, or routes. But, in the end, you wrote them as methods.

So, that was a brief overview. Now I will take a bit of a deeper dive into each term and how to use it.

Actions and Routes

These two are closely related enough that many web application developers do not realize they are different things. Or rather that they can be different things. If you are writing a .Net Core MVC application and let the code generator build your initial controllers and views will get a series of matching methods and views. Along with these methods and views, you get a route defined in your Startup method that will correctly route all of these. Because of this, if you follow the pattern set by the code generator — which I will describe below, you can think of action and route as the same.

The pattern for simple routing in a .NET Core MVC application from the code generator is the following:

app.UseEndpoints(endpoints =>

{

endpoints.MapControllerRoute(

name: "default",

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

});

If you have read any of my articles, this should look familiar since I have used it when discussing routing in “.Net Core MVC Request-Routing,” which covers routing in more detail. What this route does, in general, is to say use the base Http address of whatever server the app is running on, then look for a controller it has registered, followed by an action it has registered, followed optionally by some id. If everything is empty, default to the Home Index action. The reason this is simple is that the default controller classes constructed by the code generator have the pattern of:

public class HomeController: Controller

{

private StarChartContext _context; public HomeController(StarChartContext context)

{

_context = context;

} public IActionResult Index()

{

// Not default code, actual work

StarMapDataCounts dataCounts = new StarMapDataCounts

{

Charts = await _context.StarCharts.CountAsync(),

Systems = await _context.StarSystems.CountAsync(),

Stars = await _context.Stars.CountAsync(),

Planets = await _context.Planets.CountAsync(),

Satellites = await _context.Satellites.CountAsync(),

Atmospheres = await _context.Atmospheres.CountAsync()

}; return View(dataCounts);

} public IActionResult Privacy()

{

_logger.LogInformation($"{_className}.Privacy() Starting...");

return View();

} [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

public IActionResult Error()

{

_logger.LogInformation($"{_className}.Error() Starting...");

return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });

}

}

The critical thing to note in the above is that the controller name is <NAME>Controller. Each method is public, returns an IActionResult, and most importantly, as shown below, there are a matching <action name>.cshtml in the Views/Controller folder:

You can see from the code shown above that the methods Index and Privacy have views in the Home folder under Views, while the Error method had a view in the Shared folder. The routing subsystem will look for the View to load when your code calls the View () method starting in the matching controller view folder, then in shared unless you override the view path in the parameter — a more complex topic. So, if you stick with the simple method is an Action is a Route is a View, then you can forget about the rest of the complexities of this section. However, as I found out when I started expanding my applications, no matter how much you want to avoid the complexity, sometimes it jumps out from behind a database and clobbers you. Then you must move on to more complex routing and actions.

The idea with the additional routing information in the controller is to add more routing options to the startup configuration adding sub-actions and optional parameters in the route like this:

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}");

});

Again, I go into some additional detail in my earlier article, but generally, this allowed me to add routes that looked like:

[HttpGet]

[Route("PartialDetailsPlanets/{id}")]

[Route("PartialDetailsPlanets/{id} page={page}")]

public PartialViewResult PartialDetailsPlanets(Guid? id, int? page)

and,

[HttpGet, ActionName("AddStarChart")]

[Route("Edit/AddStarChart")]

[Route("Edit/AddStarChart/{id?}")]

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

and,

[HttpPost, ActionName("AddStarChart")]

[ValidateAntiForgeryToken]

[Route("Edit/AddStarChart/{id}")]

public async Task<IActionResult> AddStarChart(Guid? id,

[Bind("StarSystemId, StarSystemName, StarChartIds")]

StarSystemMapsStarChart map)

In the first case, I have a partial view that is part of the Details view, which I called inside the Razor view like this:

@{

_ = @Html.RenderPartialAsync("PartialDetailsPlanets", Model,

ViewData);

}

For anyone who is old enough to remember, this is the equivalent of an asynchronous update panel from back in ASP.Net 3.x, but it works consistently and is easy to use. I should write an article around this fun little construct sometime soon.

Anyway, the second route, action pair, is a sub-action of the Edit view. I called this sub-action from either the Edit or Index views. In the Edit view, the Razor code looks like this:

<a asp-action=”AddStarChart” asp-route-id=”@Model.Id”>Add to Chart</a>

And similarly, in the Index view like this:

<a asp-controller=”StarSystems” asp-action=”AddStarChart” asp-route-id=”@item.Id”>Add to Chart</a>

The main difference is that the second specifies the controller. This property is technically not needed except that there are other actions on the same View related to other controllers. So, for clarity, I include the controller for all the actions in this View.

The Http post action is called only from the AddStarChart View like this:

<form asp-action=”AddStarChart”>

In theory, both the get and post routing from above should have found the two actions without the additional information given by the ActionName attribute. In practice, I found that it did not successfully differentiate between the get and post actions, often calling the get when the intent was a post and vis-versa. Adding the action attribute onto the Http message specifier cleared this problem up completely.

The critical part to remember about route vs. action. The route is how to reach the method defining the Http address path needed by the system to route messages to reach the code that can process them. Action is the external name of your method. It is usually the final part of the route. Unless you have done something wild with your routing, if you have let me know, I want to hear about it!

Handlers

Okay, this is a big topic. I just spent four pages discussing one form of handler, the Http action handler. If I go into that much detail with all of the other types, this will be a textbook, and I am aiming at article length, not textbook length, so I will give an overview of the other types that you might use.

Configuration and startup handlers these are things like the OnModelCreating() from DbContext and ConfigureServices from Startup. Methods that are called by the base class in a defined order on an event or in sequence allowing the descendent class to make changes or updates to the base class or itself. This type is ubiquitous in .Net Core, and you probably use these without even thinking about it.

Asynchronous callbacks come in a couple of forms. The first and most common is a delegate. Anywhere you create a method with a specific signature and register it to an event on another class is a delegate. Or explicitly pass a variable of type delegate into a method to receive a callback. These are both uses of delegates. Generally, just saying, you write a method, then give a reference to that method to some other block of code along with a signature so they can call it and have something happen in your code.

There are specialized handlers of all types, hardware event handlers, timer driven handlers that wake up on a timer, and do something, handlers waiting for input from some exotic IoT device. Still, in the end, the essence of handlers is this. They are waiting for something to happen. Then performing the processing, you defined for when that event occurred. No matter what that event is, why the handler triggered, details of that trigger are essential when writing the handler but no just because it is a handler.

Methods

Okay, I am going to skim this. Methods are the code in a C# program. A method is what all the other terms I discussed today are at their Core. You write a method that can be an action, an event handler, a callback, or another method called by some other method in your application. Methods should be single purpose, but they should have a purpose.

One of my favorite stories from my time as a young programmer was the story of a company that was converting their code from one single monolithic block of code with gotos and if statements as the only flow of control. The department manager had heard that fifty (or seventy, or twenty the number varies on who is telling the story, but fifty is most common) lines if the optimal length for a function in this new-fangled language. So, he had his staff go through and break up their application at exactly fifty-line intervals and make them functions. Without changing anything else, just call the next function down at the bottom…you can probably guess how well that worked out for everyone involved.

The moral of this foolish story is that there is no defined length for a method. But if you have trouble remembering what it is supposed to do, it is probably time to break the functionality up into multiple methods. For example, I was recently working on a software project archeology and recovery project for a customer, i.e., the project would not build due to missing source files. While working on this project, I found a method that was many screens long and had many functions, some of which were apparently unrelated. I was able to understand this method without changing functionality by taking code blocks from within inner blocks of loops if statements and switch statements and putting them in appropriate methods with parameters and return values to handle inputs and outputs. After doing this, it was clear that this method was…um painful. I did not change it because that was not my project. But it essentially was a giant do everything that the program had to do and do it on global static variables method. I carefully put everything back the way it was when I started, undoing my checkout to make sure I did not change anything at all because I did not want my fingerprints on that file ever. I then finished reconstructing the missing files and moved on, glad that there had not been any issues in that file or method.

Conclusion

Well, honestly, I do not care what you call your methods. However, having everyone using standardized language helps us understand what other software engineers, programmers, developers, whatever you want to call yourselves are discussing. Standardized terminology also moves us along the transition from an art, to a craft, to an engineering profession. Right now, most of our profession is stuck in the art to craft stage, we are trying hard to reach engineering status, but there are still too many places where a single magic programmer can create art for it to be engineering yet. You do not hear many stories about some super mechanical engineer coming into General Electric and single-handedly designing a new Electric Generator that is X% better than every other on the market. That is because mechanical engineering at that level is engineering. It is well understood, and the people who study it get a similar level of competence coming out of college. Software Engineering still varies widely.

There is still this vision that a single super programmer can drop in on a project and just make it happen. In some ways, this can occur because software has only existed for less than eighty years. Think about it, mechanical engineering in one form or another has existed since when? The first blacksmith? The first flint knapper? It was not engineering, but these crafts had to have a basic understanding of forces and materials that are the Core of mechanical engineering. So, tens of thousands of years, or at least thousands of years as humanities understanding of mechanical forces evolved from mechanical arts to crafts, to engineering and science. Software and its related computer hardware have had less than one hundred years, sure we are standing on the shoulders of all the other arts, crafts, engineering, and science that came before us, but we are still at the melting tin and copper to see what it does stage. Remember that when you think of how far we have come, we are still crawling. Where will we be when we start walking or running.

Good luck, and have fun.