Written by Oskar Sjöberg on 9:th of september 2016
I’ve been involved in quite a few ASP.NET MVC and Web API projects since ASP.NET MVC´s inception and while I think the frameworks are OK for the most part, I do have a beef with controllers. So please bear with me while I rant, later on I will present a possible solution complete with code and everything, I promise!
As the controllers grow, the implementation is harder to overview and as a result, code quality usually decline. You see – it is very easy and generally risk free to bolt on just one more action method instead of changing what is already there. Even if you do your best to push code down the layers, controllers in my experience more often than not end up too large with way too many action methods.
If you have controller that is large, the corresponding test fixture, if you are well covered that is, is probably large as well. This is because to test all aspects of all action methods in a controller you need even more test methods then there are action methods in the controller under test.
Usually constructor injection is used to provide dependencies for controllers. The implication is that if MethodA requires DependencyA and MethodB requires DependencyB both DependencyA and DependencyB are present in the constructor. This means that when a request comes in for MethodA instances for both DependencyA and DependencyB are created even though only DependencyA actually is required. This is a performance problem for busy sites, but more importantly is that when creating unit tests for your action methods you will need to review the actual implementation of the action methods to determine what dependencies you actually need to provide to the constructor. Another option is to always pass all the dependencies to the constructor and don´t care whether or not they are being used but I would argue that this would not favor the readability of the tests.
I´ve always found it difficult to decide which action method should go into which controller; this is especially true if you are dealing with business event based architectures. Where do you put the action method for the OrderHasBeenShipped event?
When I later on realize that the OrderHasBeenShipped action method really belongs to the NotificationController or when GodController needs to be split into multiple controllers I now face a new problem. Since the routing is done in runtime, the compiler will not help me detect if I accidently break the URL to action method mapping. While it is possible to do automated tests for URL’s and/or do unit tests for the routing I have never seen it being done in practice to a meaningful extent.
Ok, so you skipped my rant or maybe you read the whole thing. TLDR; I find controllers annoying because I believe I have found a better solution. My variation of the MVC pattern which I am trying to introduce as MVA or Model-View-Action tries to permanently and proactively avoid the problems outlined above. The difference from MVC is minimal, simply put each action method in its own controller. This is an example on what it could look like.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}",
defaults: new { action = "Execute" }
);
This means the ~/api/greet will invoke the Execute action method on GreetController.
And a simple GreetController with a single action method looks like this.
public class GreetController : ApiController
{
public string Execute(string name)
{
return "Hello " + name + "!";
}
}
In a perfect world I would like my GreetingController to be named GreetingAction and derive from Action. While it is easy to create an Action superclass, breaking the “Controller name must end with Controller”-convention requires replacing various services of ASP.Net MVC/ WebApi /ASP.NET Core upon startup with custom implementations.
Since the ASP.NET MVC frameworks do not care in what namespace you have your controller you are free to organize your controllers into namespaces and folders any way you like.
Thank you for reading my first post! So what do you think, Is this a sensible approach? What issues have I overlooked? Please leave some feedback below!