When writing web applications I place business logic in controllers. This has worked well for my small App Dev team- our applications run reliably, perform well, and the code is easy to maintain. See below for an explanation of my design.
Contrary to my architecture, the advice I find in programming forums usually advocates for placing business logic in models. I'd like to understand why.
Based on your experience building MVC + SOA applications, why would you caution against placing logic in controllers and advocate for placing logic in models? What is your rationale and what is the pro / con tradeoff compared to my design?
I admit my design is influenced by my use of Refit, a type-safe REST proxy for C#. One of the reasons I don't want to place business logic in a model (or entity) is because I define service interfaces and entities in a Contract DLL that is shared by the service (WebAPI) and clients (MVC websites or console applications). I don't want data-persistence details leaked to the client (via Load, Save, Approve, Reject, etc methods on the entity).
Another influence comes from my desire to resolve the contradiction in these familiar design principles:
- Entities should be persistence-agnostic. That is, they should be unaware of how they're saved to a database or Content Management System (CMS).
- In MVC, place business logic in the model.
If "business logic" includes SQL or CMS statements, the principles are contradictory. If "business logic" does not include SQL or CMS statements, this implies introducing yet another layer- the Data Transfer Object (DTO). Oh God, not another layer.
Also, I feel inertia from the last popular paradigm is causing Object-Oriented (OO) design (where the object is everything) to creep into our MVC + SOA world. Hence the advice to place business logic in the model (the object).
OK, enough introduction. Here's my MVC + SOA architecture (for a Microsoft stack):
MVC + SOA Design
MVC = Model View Controller, SOA = Service Oriented Architecture
- A model transfers data and UI (validation rules, picklist choices, etc) between a controller and a view.
- A view receives a model and renders HTML using Razor syntax.
- A controller receives a model (via ASP.NET model binding) and runs business logic, either locally (2-tier architecture) or externally (3-tier architecture).
- If locally, controller communicates directly with a data store(s) (database or file).
- If externally, controller communicates with an external service(s) using Refit proxy (and the service communicates directly with a data store).
- Minimize business logic in views and models. A view should be thin, hence the name "Razor".
- Maximize business logic in controllers.
- Note the difference between SOA and OO.
- SOA emphasizes encapsulating logic in service controllers, separate from data.
- Exposed via HTTP / JSON / Refit interfaces.
- Models and entities are used for validation and data transfer.
- OO emphasizes encapsulating logic in objects, near the data.
- SOA emphasizes encapsulating logic in service controllers, separate from data.
Business Entities
- Similar to models, minimize business logic in entities.
- Separate models from entities.
- A model transfers data and UI between a controller and a view (see above).
- A model does not transfer data between a controller and a data store or service.
- This prevents over-posting (hacker adds additional form fields to override model's default values).
- This facilitates multi-page, step-by-step forms with different fields required on each page. Define a model for each page. The union of all fields on all models = set of properties on entity.
- An entity transfers data between a controller and a data store or service.
- An entity does not contain any UI (validation rules, picklist choices, etc).
- An entity may not reference a model.
- A model may reference an entity.
- Constructor accepts entity parameter, maps entity properties to model properties.
- ToEntity() method creates entity, maps model properties to entity properties.
- Dependencies flow in one direction:
- MVC Website --(depends on)--> Entity
- If local data store (2-tier architecture), MVC Website --(depends on)--> ORM (such as Dapper or Entity Framework Core)
- If external data store (3-tier architecture), MVC Website --(depends on)--> Refit Service Proxy
Rationale
- Prevention of over-posting attacks and flexibility of UI (present as complex form or split form across multiple pages) independent from entity.
- Separation of concerns- models specify validation rules and transfer data to/from UI, entities transfer data to/from service, controllers sequence tasks and enforce business logic.
- Change of backing services or data stores do not affect the model or view, only the controller.
- The logic in service controllers is reusable- exposed via HTTP endpoints and callable from any client written in any language.
- My use of Refit C# proxies in no way prevents AJAX. In fact we do plenty of UI updates via jQuery calls to service controllers, secured by JWT tokens.