Control the Controller in ASP.NET MVC

Because of its obvious importance in coordinating the MVC application, we tend to be wary of being too adventurous with it, but there are some interesting features that can sometimes be exploited to provide for less common requirements. The controller in ASP.NET MVC core, for example, can be a plain-old C# class (POCO), making it easier to create file-based content on the fly. The DefaultControllerFactory class can be changed, and you can override the GetControllerType method with a custom factory to give you localized routes and URLs. Dino explores some of the interesting internals of ASP.NET MVC controllers

The controller is at the center of an ASP.NET MVC application. Every single request, except those directed at static files, are managed by a controller. Controllers are, for this reason, subject to strict rules and conventions: For example, a controller class is expected to have a Controller suffix in the name, and also the namespace it belongs might be under-conditioned to some extent. A controller must inherit from a given base class—the Controller class—or some other class that in turn inherits from Controller. Interestingly, some of these constraints are being released in the ASP.NET Core. In ASP.NET Core, for example, a controller can be a plain-old C# class (POCO) with only a few indirect—but still necessary—connections to the ASP.NET infrastructure. In this article, I’ll go through a few little-explored aspects of ASP.NET controller classes, across classic ASP.NET MVC and ASP.NET Core. I’ll touch on a range of topics including POCO controllers in ASP.NET Core, ASP.NET controller factories and localizable routes

ASP.NET Core POCO Controllers

The most common way to create a valid controller class that the system can easily discover is to give the class name the suffix “Controller” and inheriting it from the aforementioned Controller base class. This means that the corresponding class of a controller called ‘Home’ will be the HomeController class. If such a class exists, the system is happy and can successfully resolve the request. This is the way that things worked in past versions of ASP.NET MVC before ASP.NET Core. In ASP.NET Core the namespace of the controller class is unimportant, although the tradition is maintained by tooling that continues to place controller classes always under a folder named Controllers. In fact, you can now place your controller classes in any folders and any namespaces that you wish. As long as the class has the “Controller” suffix and inherits from Controller, it will always be discovered.

In ASP.NET Core, however, the controller class will be successfully discovered even if it lacks the “Controller” suffix. There are a couple of caveats, though. The first caveat is that the discovery process works only if the class inherits from base class Controller. The second caveat is that the name of the class must match exactly the controller name as it resulted from the route analysis. For example, if the controller name extracted from the route is “Home” then it is acceptable for the system if you have a class named Home that inherits from base class Controller. Any other name won’t work. In other words, the system needs two distinct pieces of information to discover a controller. First, the system needs to know if the class can serve as an ASP.NET MVC controller. Second, the system needs to know which controller route the class can serve. A class is recognized as a controller if it inherits from Controller, ends with the suffix “Controller” or is marked with the [Controller] attribute, as below.

The controller route parameter is the name of the class or the part of the name that precedes the suffix “Controller”. Therefore, also the following class is acceptable as a controller and its nickname will be “home”.

Why is a POCO controller an option within an ASP.NET MVC application? All in all, a POCO controller is a form of optimization that reduces overhead and the memory footprint. Not inheriting from a known base class cuts some functions off that might not be strictly necessary. At the same time, not inheriting from a known base class may preclude some common operations or make them a bit more verbose to implement. Let’s review a few examples where it is acceptable or even recommended to have a POCO controller.

A POCO controller works well if you don’t need any dependencies on the surrounding HTTP environment. If your task is creating a super-simple web service that just returns JSON data, then a POCO controller may be a good choice. The following code, in fact, works just fine.

A POCO controller also works well if you have a bunch of functions dealing with the content of some files, whether existing or to be created on the fly. Additionally, you can use a POCO controller to return HTML that is composed algorithmically.

If you want to generate HTML via the Razor engine instead, it is a different story. In this case, more work is required and, more importantly, it requires more in-depth knowledge of the framework.

The aspect of a POCO controller that causes problems is the lack of the HTTP context. In particular, this means that you can’t inspect the raw data that is being posted, including query string and route parameters. However, model binding brings sufficient data your way without the need of direct access to the HTTP context. There’s definitely a way to plug the HTTP context in, but at this point you need to question why you are using a POCO controller.

Controller Factory

In ASP.NET MVC and in ASP.NET Core, every request that doesn’t result in serving a static file like an image, a CSS or a JavaScript file ends up being mapped to a controller class and produces its output executing one of the controller’s public methods. The instance of the serving controller class is created through a factory class. The factory class is responsible for the following actions:

  • Getting you a controller instance
  • Releasing an existing controller instance

Both in ASP.NET MVC and ASP.NET Core, there’s a class called DefaultControllerFactory that contains the default implementation used by the system. This class can be replaced if necessary. In ASP.NET Core, you replace it via the internal Dependency Injection (DI) system.

Let’s see how to do that in classic ASP.NET MVC.

The ideal place for this code is in the Application_Start event handler within global.asax. A custom controller factory, especially if it derives from DefaultControllerFactory can do one more thing. It can determine the actual type being used for a given controller nickname. This opens up at least a couple of interesting opportunities for use, one just fancy and one quite more intriguing.

One Fancy Thing You Can Do with the Controller Factory

You can use a custom controller factory to change the default rule for controller names. In classic ASP.NET MVC, if you don’t like to have classes named with the Controller suffix, then you can change it to whatever else you like. You can even localize it. Admittedly, this is not one of those things with a concrete application in the real world or, at least, I haven’t found one yet that is really worthwhile. Yet it is a funny thing to try to play with the internal machinery of ASP.NET.

The GetControllerType overridable method returns the Type object to be created against the provided controller nickname. If the passed name is, say, “Home” then the above implementation will return a Type object for the YourNS.Controllers.HomeCoordinator class. Needless to say, the HomeCoordinator class will have exactly the same structure of a regular controller class but a quite unusual name.

Localizing the Routes of the Application

Frankly, I wrote the above demo many years ago while authoring a programming book about a freshly released version of ASP.NET MVC and it never moved out of that old Visual Studio 2010 project. Recently, instead, I got a question from a customer asking another apparently weird thing that, at the end of the day, I could only answer by resorting to a custom factory that just overrides the GetControllerType method.

The question was: ‘how can I have localized routes and language-sensitive URLs in my application?’ The customer had a multi-lingual application and wished to present each user with URLs expressed in their own language of choice. For example, the canonical URL home/index had to become hem/index in Swedish and product/catalog was expected to be something like produkt/katalog. The same was needed for a few other supported languages. Needless to say, the customer was willing to have only one controller and only one action implementation. Something had to be created that could manipulate the request and route to the sole existing controller written for the neutral language—in the specific case, the English language.

I run into the following project http://github.com/Dresel/RouteLocalization but at first I found it far too intricate to adopt, and possibly expand. Also, it seemed to me that the project was blinking at attribute routing whereas the project was using conventional table-based routing. Whether I was right or wrong,
I felt that this project needed an easier, and more direct, way to localize routes and URLs. Immediately, the GetControllerType method of the default controller factory class popped up in my mind, and so I ended up writing a custom factory like this.

I made some assumptions about the resource files available in the application and introduced some conventions to keep the code lean and mean. In particular, I assumed that we’d need to have controller and action names localized to a resource file. Let’s say, routes.resx for the neutral language, routes.se.resx for the Swedish language and so forth. The figure below shows the content of the Swedish resource file.

As you can see, all action names have the form Action_XXX where XXX is the value for the same entry in the neutral language. Needless to say, XXX is the name of the action method actually implemented. Controller names follow a similar pattern Controller_XXX and XXX is the name of the sole controller actually implemented.

The parameter controllerName of the GetControllerType method receives the controller name as the routing system has determined it. If the requested URL is, say, produkt/katalog then the parameter is set to “produkt”. Helper methods GetNeutralControllerName and GetNeutralActionName map the resource value to the resource name and pick up Controller_Product in front of Produkt and Action_Catalog in front of Katalog.

In the .NET Framework, it is easy to read the value of a resource. It is trickier to get the key from the value. You must retrieve the resource set dictionary via the ResourceManager embedded in the auto-generated resource object. If an entry is found that matches the localized controller name, its key is taken and the prefix “Controller_” stripped off. Exactly the same code runs for action methods except that a different prefix is stripped off at the end.

It should be noted that while the controller name is also passed as an argument, the action name must be read from the RouteData collection. (Also, the controller could be read from there.) More importantly, the RouteData collection must be updated with the neutral controller and action names for the remainder of the ASP.NET infrastructure to successfully invoke the method and locate any related views.

If you focus again on the implementation of the GetControllerType method you see that there’s no need for modifying the actual process that produces the actual Type object. Once we changed controller and action name the default behavior can be preserved. Some further comment deserves the following line:

This is necessary to properly handle the empty route (i.e., http://yourserver.com) and to make the system accept anyway any URLs expressed in the neutral language. The figure shows a few screenshots from a sample application.

The first screenshot handles correctly a Swedish URL and maps it correctly to neutral language route parameters. As long as the UI culture remains set to Swedish only Swedish and neutral names will be accepted in the URLs. The central screenshot shows how the system defaults to the neutral language regardless the UI culture set when no URL is explicitly provided. Finally, the third screenshot shows what happens when you switch the language to Italian. If you try to use a Swedish URL when the language is Italian you’ll get a 404 error and vice versa.

Summary

The article started with an overview of controllers in ASP.NET and ASP.NET Core and ended with a discussion about localized routes and URLs. Frankly, I don’t know how many applications out there have the need of using localized routes but I hope that at least implementing them—even only for fun—contributed to unveiling some interesting internals of ASP.NET MVC.