Having adopted .NET Core while it was in beta and using it to implement numerous projects over the last three years, I’ve settled on an MVC + SOA architecture. I say settled because I don’t know if I can claim to have developed it. I don’t how original it is. Anyhow, this is the architecture I use in my projects.
Below the diagram I define terms and describe the principles of my architecture.
|MVC||Model View Controller|
|SOA||Service Oriented Architecture|
|JWT||JSON Web Token, a claims-based security mechanism|
|CORS||Cross Origin Resource Sharing, a web browser security mechanism|
|Razor||Microsoft’s web UI markup syntax|
|DTO||Data Transfer Object|
|Model Binding||Microsoft’s technology for translating an HTTP request stream into a C# class instance sent as a parameter to a controller action|
|Refit||A library that creates strongly-typed C# proxy classes for invoking web service methods|
|Contract DLL||A class library referenced by both client and server. Contains definitions of entity classes and service interfaces.|
|Web API||Microsoft’s version of MVC for creating non-UI Application Programming Interfaces (APIs)|
|Dapper||A high-performance Object Relational Mapper (ORM) that enables developers to write SQL instead of LINQ. See my blog post, The Best Domain-Specific Language for Manipulating Data is SQL.|
|Simple Variant||For small applications, controllers connect directly to data sources|
|Complex Variant||For large applications, controllers connect to one or many microservices|
- A model transfers data and UI (validation attributes, 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 (simple variant) or externally (complex variant).
- If locally, controller communicates directly with a data store(s) (database or file).
- If externally, controller communicates with an external service(s) using a Refit proxy (and the service communicates directly with a data store).
- Minimize business logic in views and models.
- Maximize business logic in controllers.
- Note the difference between this Service Oriented Architecture (SOA) and Object Oriented architecture (OO).
- SOA emphasizes encapsulating logic in service controllers, separate from data.
- Exposed via JSON and Refit interfaces.
- Models and entities are used for validation and data transfer.
- OO emphasizes encapsulating logic in objects, near the data.
Comparing Models and Entities
- Similar to models, minimize business logic in entities.
- Separate models from entities.
- Define models in website source code. Do not expose models to any other projects.
- Define entities in Contract DLL which is referenced by both website and service. Additionally, the Contract DLL may be referenced by other clients, such as console applications, PowerShell scripts, or Windows services.
- 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 an over-posting attack (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. 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 attributes, picklist choices, etc).
- An entity may not reference a model.
- A model may reference an entity. However, it may not expose the entity as a class property (as this makes it vulnerable to over-posting).
- Model’s constructor accepts entity parameter, maps entity properties to model properties.
- Model’s ToEntity() method creates entity, maps model properties to entity properties.
- Dependencies flow in one direction.
- MVC website depends on entity.
- Simple Variant: MVC website depends on ORM (such as Dapper or Entity Framework).
- Complex Variant: MVC website depends on Refit proxy.
Overuse of Object Oriented Design
I expect the highlighted statement in Architecture Principles above to be controversial. Many architects advise programmers to place business logic in domain classes, not directly in controllers. However, I believe a lot of Object Oriented orthodoxy has been unwisely carried forward into our modern world of Service Oriented Architecture. In my opinion, this is a violation of the warning to “not let let a prior implementation become a future requirement.”
Don’t get me wrong- I’m not advocating to never write OO domain classes to encapsulate business logic. OO techniques help tame overly complex business logic. I’m just saying if the domain logic can be expressed succinctly using procedural code in controller actions, then write it that way. Don’t feel obligated to create unnecessary OO abstractions. Perhaps my experience writing a chess engine has influenced my view that OO techniques are not necessary to write high-performing, maintainable code.
Since the entry point of a microservice is a URL endpoint (not a DLL); and web services make no assumptions about client operating system, programming language, or application framework (their primary value proposition is they’re language agnostic); therefore the objects deserialized from JSON messages passed to and from clients and web services will not exhibit Object Oriented behaviors such as encapsulation and polymorphism; it follows that web service client and server code at the endpoint boundaries is written in a procedural style. If that is the case then why not carry the style all the way through? Why do programmers feel a compulsion to immediately wrap deserialized DTOs in intelligent objects, heavy with OO abstractions? Why not simply write procedural code that references the DTOs to implement the client or service logic?
You may have noticed I do not mention REST anywhere. I find the term vague and devoid of meaning. There was a time, ten years ago, when the term REST was used to describe the payload of a web service call. Saying a service was a REST web service distinguished it from a SOAP web service. However, the industry abandoned SOAP many years ago.
Perhaps the the World Wide Web Consortium (W3C) is embarrassed by the complexity of the SOAP protocol because I could not find a page on their website with a comprehensive list of SOAP family specifications. I did, however, find a list on Microsoft’s Docs website: Web Service Protocols Interoperability Guide. Microsoft, a SOAP committee member, implemented all these SOAP specifications (“the horror, the horror”) in its predecessor to ASP.NET Core Web API, Windows Communication Framework (WCF). Microsoft lists a dizzying array of technologies. Admittedly, not all specs are managed by W3C. Some are managed by Oasis.
- SOAP Part 1, Part 2
The industry resoundingly rejected this as too heavy-handed an approach. It abandoned the strict type system and protocols of SOAP in favor of the Wild West of schema-free JSON web services, unconstrained by anything other than the JSON Data Interchange Syntax.
OK, that was a long detour made to reinforce my point: the term REST used to carry meaning. It no longer does. Well, it carries meaning only among fanatics– the developers who lecture about highfalutin requirements that must be met to earn the label, RESTful service. They seem to miss the point of the industry’s vote (1): We don’t want heavy-handed schemas, specs, religious orthodoxies, or lectures on purity. To the modern programmer, REST means HTTP + JSON + informal documentation preferably in the form of example code. That is all. We view REST as a lightweight, agile technique for connecting distributed systems.
In the diagram and principles above, I advocate we adjust the model-heavy MVC pattern we inherited from fat client GUI architecture in order to achieve separation of concerns in the web UI applications we are building today. Achieving a clean separation of concerns is necessary in order to construct comprehensible source code that expresses design intent, which is helpful for programmers who maintain the code. I advocate we achieve this in modern web UI applications by using an MVC + E (for Entities) pattern, locating simple business logic in controller action methods. If the business logic is complex, then implement it in Object Oriented domain classes and reference the domain classes in the controller action method.
- A Model is a DTO between a View and a Controller.
- An Entity is a DTO between a Controller and a service or data source.
- A Domain Class is an OO construct used internally by services and utilities, never publicly exposed.
In addition, I advocate for embracing the unconstrained nature of JSON web services and not wading into religious arguments refereed by REST purists about the proper use of HTTP verbs and media types. However, it is possible to constrain JSON web services (if you control both client and server) by writing a C# interface and associated entity classes, publishing it in a Contract DLL, referencing the Contract DLL from both client and server, and leveraging Refit to create type-safe proxy classes. This Contract DLL should contain the service interface and entity DTOs only- no domain classes.
(1) When I say “vote” of course I don’t mean a literal vote with candidates, ballots, and electioneering. I mean the industry voted with its code. Programmers stopped writing SOAP and starting writing JSON.