Why don’t we write code in the controllers?
I think many of you have heard the opinion that there should be no code in the controllers, and therefore a controller with methods in one line is considered “Best Practice.” I, in turn, doubt that the benefits of this are so great. If you had similar thoughts, I ask under kat.
Hello everyone! I want to say right away that my opinion is not true, and the purpose of this post is to express my opinion and hear the comments of others. All of the above applies to the implementation of the API, if you have MVC with views, then this case will not work, because in this case it is better to write only the logic with View in the controllers
I’m sure that many of you are working with a typical CRUD application with 3-layer architecture (by the way, there are no more layers, but more about that next time). And in this architecture, you have a layer for working with data (further DA), a layer of business logic (further BL), and a view layer (further VL).
BL can be done in different ways, I met 2 options
- Class is just a class that has a bunch of dependencies and methods. Each method describes a business flow, for example, money transfer, authorization, registration. This class uses lower-level things such as IRepository for working with databases, various API clients for other services and the like, in general, on this layer they put all the modules together and make business logic.
- CQS – For each flow business, DTO (Command \ Query) classes are created, these are just the input parameters to our handler. This method becomes more popular as responsibility is better shared and this handler does not have so many dependencies.
These methods have a number of rules of their own and one of them is that the handlers cannot call other handlers, they must be completely independent, the exact same situation in the implementation through an ordinary class with a bunch of methods, the methods are so attached to the feature that they cannot be reused.
Consider a couple of implementations in this style, and then I will transfer the code to the controllers and we will analyze what we have lost.
In this form, we implement all the methods and then simply call them at the API level.
I hope you understand that the code with Command -> Handler will be similar, just more divided.
And it always haunts me, why am I doing this extra work?
- To reuse the code? – No, the flow of money transfer is not suitable for the flow of bonuses.
In addition, if you even find the opportunity to combine 2 features, you risk breaking one – automatically breaking another
- To transfer a code call to another place? “Perhaps, but does it happen so often?” Yes, and it’s not necessary to transfer, who forbade you to resolve the instance of the controller closed under IUserService?
- Testing? Controllers are tested the same way, and by raising TestServer you can practically write end2end tests.
Now let’s look at black magic
Let’s remove the excess.
Miraculously, the service code does not differ from the controller, we just added a couple of attributes and are ready to catch http requests.
I believe that ASP NET has perfectly abstracted us from working with HTTP, we have the best place where we operate with our types. I repeat, if you have views, then in controllers it is better to write code only for View, and in services write reused methods to get data for View. But in current realities, more and more often we have API + SPA
ASP NET Core Pipeline is very well tuned and has a lot of solutions, take a look at FluentValidation, you will add validation without even changing the code in the controllers.
Want more separation?
Separate the interface and if you need an implementation too.
As a bonus, the service interface becomes a top-level contract, and within the framework of one process it is just a direct call of the code from the controller, within the framework of client-server communication, a simple implementation of the same interface using HttpClient is substituted.
Connect other channels
If they tell me that we can have another channel, for example, through a queue, I can just get an instance of the controller and use it in another channel. This controller easily resolves from DI. In addition, ASP NET is quite flexible and you can teach some channels to process it yourself, again modifying the pipeline.
A test task in this style will be thrown out and rejected, but you have a lot of reasons why you did it, but if you go to an interview and discuss why you’re designing the code, then this is probably a good place and you have the same specialist who understands that programming is multifaceted
You can reuse only services by type of repository, cache, client api, etc., but reuse the logic of processing a single request is a very rare case, and most likely a bad one.
This approach, like others, needs to be applied in the right places, it saves time, and is very convenient for APIs (microservices) that are closed to public access.
I believe that the controllers are the very BL Service or Command \ QueryHandler, and in my projects I practice this approach and the controllers do very well, I recommend that you try.
We didn’t increase the responsibility of the framework, he had a task
- Accept requests
- Map them in a model
- Call the code we specified according to some rules
- Give answers
He stayed with him.
I really do not see critical problems why this should not be done, therefore I invite you to comment.
If I’m not thrown much, then the article will be about the interfaces on each service class of the system, and is this necessary? Is it worth writing a solution in order to make it easier to