Solution Architecture - rorymacleod/Toolbox GitHub Wiki
Tidy Architecture
Clean Architecture, coined by Robert C Martin, is an architectural that emphasises separation of concerns and tight control of dependencies between components (reading Uncle Bob's brief introduction is recommended). It is often illustrated with this diagram:
Tidy Architecture is not as good as Clean Architecture, but it is more pragmatic, easier to implement, and easier to live with. It stays consistent to the principles in Uncle Bob's description, while making the following adjustments:
- To work better in ASP.NET Core controllers, there are no "ports and adapters" around use cases. The use case object is passed parameters wrapped in a Command object, and optionally returns a Response object.
- To work better with Entity Framework, the entities do not contain business logic, and the division between the entities and the database is somewhat blurred.
Interaction
Business logic
The application's business logic is divided into use cases and implementors. The use cases are the entry points, and orchestrate the work of the implementors. Implementors are discrete units of business logic; reusable, testable, and self-contained.
The business logic layer:
- Is stateless and thread-safe.
- Depends on entities and the domain model, but is not aware of controllers, the web framework, message queues, etc.
- Accesses data through data access objects that are injected as interfaces. Does not directly call Entity Framework or the database. Does not contain any database queries.
- Is grouped into folders and namespaces by feature, e.g., Order Import, Conflict Resolution, Cart Checkout.
- Is implemented in separate projects in the solution. For a simple application, there may be a single Business class library, or the code could be split across several libraries, by feature.
Use cases
Use cases are the entry points to the application's business logic.
Use cases are invoked using the "mediator" pattern, specifically, using the Mediatr NuGet package. For each use case there is a corresponding Command object: when the command is passed to Mediatr, it identifies the use case, instantiates it using dependency injection, and invokes it with the command as a parameter.
Mediatr provides a "pipeline", where cross-cutting concerns can be implemented, e.g., logging and exception handling.
The use cases:
- Are named in a VerbNoun style, e.g.,
AddUser
,RemoveTransaction
,ImportTemplate
. - Are named using terms the business would understand, and scoped to correspond to use cases or user stories provided by business analysts.
- Do not call other use cases. If necessary, the interactor can call a series of use cases, but generally, a use case should encompass a single logical operation from beginning to end.
- Are tested by integration tests.
The pipeline that calls the use cases:
- Logs every invocation of a use case.
- Handles and logs any exceptions. It could turn an exception into an error message to be displayed, or abandon or retry a queued message.
- Performs authentication and authorization (if that's not already handled by the web framework).
- Can validate the command's parameters.
Implementors
Integrations
Data model
Data access