The Solen LMS is an open source Learning Management System. It's built using the latest version of ASP.NET Core
. The application was
designed and developed following the principles of the Clean Architecture as defined
by Robert C. Martin (aka Uncle Bob) in his
book Clean Architecture.
Table of Contents :
- Azure Environment Architecture
- Clean Architecture (Quick Overview)
- Solen LMS Architecture
- Contribution
The idea of Clean Architecture, is to put the Business Logic and Rules (aka
Policies
) at the centre of the design, and put theInfrastructure
(akamechanisms
) at the edges of this design.
The Business Rules are divided between two layers: the Domain
layer (aka Entities) and the Use Cases
layer. These
two layers form what is called the Core
of the system.
What makes this architecture work is that all dependencies must flow inwards. The Core
of the system has no
dependencies on any outside layers. The other layers depend on Core
, but not on one another.
Source code dependencies must point only inward, toward high-level policies.
This is the architectural application of the Dependecy Inversion Principle
.
A Clean Architecture produces systems that have the following characteristics :
- Independent of frameworks.
Core
should not be dependent on external frameworks such asEntity Framework
. - Testable. The business rules can be tested without the UI, database, web server, or any other external element.
- Independent of the UI. The UI can change easily. For example, we can swap out the
Web UI
for aConsole UI
, orAngular
forBlazor
. Logic is contained withinCore
, so changing the UI will not impact the system. - Independent of the database. We can change
SQL Server
forOracle
,Mongo
, or something else.Core
is not bound to the database. - Independent of any external agency.
Core
simply doesn't know anything about the outside world.
For further reading about Clean Architecture, I highly recommend this book.
The Solen LMS solution is composed of three applications :
- the Api (aka Backend) contains the business rules and does the heavy lifting.
- the Web Client (aka Frontend) contains the UIs to interact with the users. It's built with
blazor Webassembly
. - and the Identity Provider (implemented by Identity Server) responsible for creating, managing and authenticating the users.
I could of course separate those applications into different solutions and different repositories, but in my opinion, as long as the team size working on the project remains small, it's more convenient to keep them in the same repository and the same solution.
Since the most important code resides in the API application, we'll focus more on its structure.
Let's take a look at the content of the API structure :
When we look at the top-level directory structure, we can guess what the application does. By reading the names of
the sub-directories, we can tell the propose of creating this application which is a Learning Management System.
A good architecture of a software application should, in the first place, scream about the use cases of the
application, not about frameworks and tools. This is what Uncle Bob calls in his book Screaming Architecture.
Each sub-directory of the API directory represent a module, an independent module of the application. Each module is composed of four layers :
- Domain Layer
- Use Cases Layer
- Infrastructure Layer
- Presentation Layer
The Domain
layer contains the business objects called Entities. Those objects encapsulate the domain concepts within the context of the module
(ex: Course, Instructor, Learner, Category..) and grouped into different Aggregates. An Aggregate, in Domain Driven-Design
terminology, is a cluster of domain objects that can be treated as a single unit.
This layer encapsulates and implements all the use cases of the module. A Use Case is an object that handles the users requests and applies rules
that govern the interaction between the users and the Entities.
Each Use Case is independent of the others (Single Responsibility Principle
). For example, in the Course Management module,
modifying or deleting the Use Case UpdateCourse
will have absolutely no effects on the DeleteCourse
Use Case.
The Use Cases are grouped by the same logic as the entities, i.e Aggregates.
To tackle business complexity and keep *Use Cases" simple to read and maintain, the Use Cases Layer implements the architectural pattern CQRS. Using this pattern means clear separation between Commands (Write operations) and Queries (Read operations). This separation gives us, we developers, a clear picture of what pieces of code change the state of the application.
To keep the application business rules out the external layers and to prevent this layers from knowing much about the Business Logic, the Use Case Layer implements the Mediator Pattern.
To implement the CQRS
Pattern and the Mediator
Pattern easily, the Use Cases Layer uses an open source
.NET library called MediatR. It allows in-process messaging and provides an
elegant and powerful approach for writing CQRS
and sending events between layers and modules.
When exposing public APIs, it is important to validate each incoming request to ensure it meets all expected
pre-conditions. The system should process valid requests but return an error for any invalid requests.
The validation process is part of the Use Cases business logic. Therefore, the responsibility for validating requests
does not belong within the Web API
or Console UI
or whatsoever external interfaces, but rather in the Use Cases Layer.
To make the validation process easier, I use a popular .NET library for building validation rules
called FLUENT VALIDATION.
The other advantage of using this library, is we can make use of MediatR
pipeline behaviours to validate automatically
every request that requires validation before further processing occurs.
The Infrastructure Layer implements all interfaces (abstractions) used by the Use Cases Layer to persist data, access external systems.... It contains also all configurations related to the Dependency Injection Container.
The Presentation Layer is the entry point to the system from the user’s point of view. Its primary concerns are routing requests to the Use Cases Layer.
This layer exposes public Web APIs. All the concerns related to the GUIs
are handled by the Web Client application.
In the "traditional" way to write controllers, we usually implement some business logic flow in like as Validation, Mapping Objects, Return HTTP status code...
Example of a Fat Controller :
[HttpPost]
public async Task<IHttpActionResult> CreateCourse(CourseModel model) {
if (!ModelState.IsValid) return BadRequest (ModelState);
var course = new Course {
Title = model.title
};
var result = await _coursesService.CreateCourse(course);
if (!result.Succeeded) return GetErrorResult (result);
return Ok ();
}
By implementing the Clean Architecture (where all the business logic and rules are implemented in the Core Layer),
and with the help of a library like Mediadtr
, we can write controllers with few lines of code :
For the moment, I will be the only contributor of the project. Nevertheless, you're welcome to report bugs or/and submit features by creating issues.