- Home>
- .NET core>
- Notes on The Clean Architecture
In this post, I’m going to share my understanding of clean architecture after reading about it in the book “Clean Architecture” and other posts, as well as how I implement the clean architecture in an ASP.NET core project.
Clean architecture is an architecture in which the high level business rules and policies do not have a compile-time dependency on low level components that deal with external systems.
Consider the image below, which I take from the Clean Code Blog.
In the above image, the high level business rules and policies reside at the two innermost circles: Entities and Use Cases. The low level details reside at the outermost circle: Devices, Web, UI, External Interfaces and DB.
The above image is just a representative of a clean architecture. In practice, a project may have more or less number of layers, and the layers may have different names. For example, in the ASP.NET core application that I am working on, the solution consists of the following projects that correspond to the layers in the above picture.
Using the above layers as an example, the domain and core layers make up the entities and use cases in my application. If you look at the arrows in the picture, the arrows point inward, from the outermost circle to the innermost circle. The direction of the arrows signify that the outer layers may know about (have dependency on) the inner layers, but not the other way around.
In clean architecture, you want to keep the details, low level abstract away from the use cases. This allows you to replace the lower level implementations without affecting the use cases. To make this work, the higher layers should not have any dependency on the lower level layers. For example, the core layer knows nothing about the infrastructure layer. However, the infrastructure may depend on the core layer because it needs to aware of the use cases to implement the interfaces defined at the core layer. As such, in circular diagrams that demonstrate layers in a clean architecture, the dependency direction points inward, from the outer circles to the inner circles. The outermost circle represents the lowest, farthest away from the use cases layer. An application that properly follows the clean architecture is easier to maintain, reason and test. Because the use case layer does not depend on any external system, ideally, you can swap the infrastructure layer or change the implementations in the infrastructure layer without affecting the use case layer.
We do not expect changes in [use cases] to affect the entities. We also do not expect [use cases] to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. The use cases layer is isolated from such concerns.
The Clean Architecture
In my application, the infrastructure project has dependencies on the core and domain projects. Neither core nor domain have a dependency on infrastructure or web API, at least not at compile time.
At compile time, core and domain do not depend on infrastructure or web api. At run time, however, core and domain still need to communicate with infrastructure to interface with the database and external systems. The communication is indirect and via mediator objects. For example, in my application, core and domain communicate with infrastructure through the web api, which acts as a mediator.
Consider the code snippet below of the controller endpoint which the UI calls to retrieve the terms of use:
[HttpGet("GetTermsOfUse")] public async Task<TermsOfUseDTO> GetTermsOfUse(int type) => await _mediator.Send(new GetTermsOfUseRequest((int)TouType.MediaAgreement == type ? _mediaAgreementOptions.Name : _disclaimerOptions.Name));
Several things happen behind the scene to retrieve the terms of use from the database.
One thing I want to point out is that the interface of the handler is defined at the use cases (core) layer. However, the implementation is at the infrastructure layer.
Had I call the handler’s implementation directly, I would have an unnecessary coupling between the web API and the infrastructure layer. This would not be consistent with the clean architecture because the web API is at the higher level than the infrastructure layer. In his post on clean architecture, Robert Martin states:
The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.
The Clean Architecture
In fact, note that the result of the method, TermsOfUseDTO is a POCO object which has no dependency on external frameworks. Following the idea of the dependency inversion principle and mediator pattern, the web API layer communicates with the use cases layer to process the request. At run time, execution ends up at the infrastructure layer, thanks to .NET dependency injection (DI) container which allows me to set up implementations for the interfaces at runtime.
Below code snippet shows the interface of the handler, which is at the use cases layer. Note that besides extending from IRequest which is part of MediatR, GetTermsOfUseRequest consists only of a string property. This is consistent with the principle that the high level policies do not have a source code dependency on low level details.
namespace Core.Requests { public record GetTermsOfUseRequest(string TouName): IRequest<TermsOfUseDTO>; }
Below is the implementation, which is at the infrastructure layer.
public async Task<TermsOfUseDTO> Handle(GetTermsOfUseRequest request, CancellationToken cancellationToken) { var termsOfUse = await _dbContext.TermsOfUse .Where(row => row.Description == request.TouName) .AsNoTracking().FirstAsync(); return _mapper.Map<TermsOfUseDTO>(termsOfUse); }
In the above snippets, the LINQ expression returns an entity object. However, this object is not suitable for returning to the UI, as the entity is a model of a table row in the database. Therefore, I convert the result into a POCO object.
public class TermsOfUseDTO { public int Id { get; set; } public string Content { get; set; } public string DisplayName { get; set; } public string Description { get; set; } public DateTime CreatedDate { get; set; } }
An n-tier architecture can be open or closed. In an open n-tier architecture, a layer can call any layer below it. In a closed n-tier architecture, a layer can only call the layer immediately below it. As an example, if I were to implement n-tier architecture in my application, the use cases layer, or a web API could have a dependency on the infrastructure layer. To my understanding, this would be less flexible because changes to the infrastructure might have a direct impact on the use cases.
As we have seen, in a clean architecture, the use cases and entities do not have a source code dependency on low level details.
Clean architecture vs onion architecture
Common frameworks, libraries and design patterns I use
Notes on Component Cohesion
Building a fully multitenant system using Microsoft Identity Framework and SQL Row Level Security
Analyzing a rental property through automation
Web scraping in C# using HtmlAgilityPack
Building multitenant application – Part 2: Storing value into database session context from ASP.NET core web API
Build and deploy a WebJob alongside web app using azure pipelines
Authenticate against azure ad using certificate in a client credentials flow