- Published on
Implementing Clean Architecture in ASP.NET Application
Table of Contents
Clean architecture was proposed by Robert C. Martin (Uncle Bob). It is a distillation of various architectures such as Hexagonal, Onion architecture and others.
The essence of the architecture lies in:
- Layers
- Dependency Rule
Layers
At the heart of this architecture lies the notion of layers. A layer is a grouping of related functionalities within an application. It's a way of structuring the codebase that ensures separation of concerns. Also, these layers act as a guard, ensuring that changes from one layer do not ripple across entire system.
In clean architecture, we divide our code into following layers:
- Domain
- Application
- Infrastructure
- An Execution Layer
Domain
As the name suggest this layers encapsulates the domain of the software we are trying to build. It is independent of any external concern such as database, storage or any other external library.
We model Domain layer using concepts from Domain Driven Design (DDD) and it might include following types of classes:
- Entities
- Value Objects
- Domain Services
- Domain Events
For example, if were building an ecommerce platform then we would have defined our domain using classes like these:
Type | Classes |
---|---|
Entities | Product, Customer, CustomerCart, Order |
Value Objects | ProductColor, ProductWeight, EmailAddress |
Domain Events | ProductPurchased |
Application
In Application Layer, we implement application usecases. Continuing with our ecommerce example, some of the usecases would be:
- Displaying best-selling products
- Adding a product to the shopping cart
- Adding a product to the wishlist
The Application Layer acts as a mediator between the Domain Layer and the Execution Layer. It takes inputs from the Execution Layer and then orchestrates and coordinates the workflow by triggering the appropriate actions within the Domain Layer.
Being an orchestration layer, the Application Layer solely instructs objects from the Domain Layer, with all the underlying functionality provided by the Domain Layer itself.
Also, if we need any external dependency then we define contracts in the form of interfaces for those dependencies in this layer which are then implemented in Infrastructure layer. This helps us enforce dependency inversion principle on external dependencies. Below are some of the examples of contracts:
- IProductRepository
- IAuthService
- IPaymentService
This layer is also responsible for handling cross-cutting concerns, like Authorization, Validation and others.
public async Task<ApplicationResult> AddProductToCard(AddProductToCartCommand cmd)
{
var result = new ApplicationResult();
if (cmd.ProductId is null)
{
result.AddError("ProductId should not be null.");
}
if (cmd.UserId is null)
{
result.AddError("UserId should not be null.");
}
if (cmd.Quantity < 1)
{
result.AddError("Quantity should be greater than 0.");
}
if (result.ErrorsPresent)
{
return result;
}
var product = await productRepository.Get(cmd.ProductId);
if (product is null)
{
result.AddError("Product does not exists.");
return result;
}
var userCart = await userCartRepository.Get(cmd.UserId)
if (userCart is null)
{
userCart = new UserCart(cmd.UserId);
}
userCart.Add(product, cmd.Quantity);
await userCartRepository.Save(userCart);
result.Data = "Product added to cart!";
return result;
}
Infrastructure
We host all the necessary external dependencies within the Infrastructure Layer. This layer acts as a home for various libraries required to interact with external systems, services, or databases.
In Application Layer we define contracts and those are implemented in Infrastructure layer following Dependency Inversion principle. These contracts serve as blueprints for functionality we require from these external services.
When it comes to structuring the Infrastructure Layer, we have some flexibility. We can opt for a single, cohesive Infrastructure Layer that houses all our external dependencies. Alternatively, we can take a more specialized approach by creating multiple Infrastructure sub-layers, each catering to a specific concern. For example, we might have an Infrastructure.Database layer dedicated to handling database-related dependencies, and an Infrastructure.Payment layer dedicated to payment-related dependencies.
Execution Layer
The Execution Layer is the final layer in our Clean Architecture and it executes our code.
At its core, the Execution Layer is responsible for facilitating input and output for the application's use cases. It acts as a gateway that communicates with external systems or users, allowing them to interact with the application's functionalities.
Thanks to our adherence to Clean Architecture principles, we can have as many Execution Layers as we desire, all without making any alterations to the existing layers. The flexibility is simply fantastic!
For instance, we can create diverse Execution Layers, such as WebAPI, Console CLI or UI applications, each catering to distinct requirements or user experiences. This modular approach enables us to seamlessly introduce new I/O interfaces to the application without disturbing the rest of the architecture.
Why do we divide code into layers?
Improved Testability
Testing the behavior of our application is a critical aspect of software development. Without dividing the codebase into well-structured layers, managing tests can quickly become a daunting and error-prone task. That is because when the distance between the input and output of a test becomes too large then it becomes challenging to pinpoint where things might go wrong, leading to inefficient testing.
However, with the division of our codebase into cohesive layers, each layer then exposes specific functionality which allows to test each layer easily.
Greater Flexibility
We get greater flexibility by correctly ordering the layers. The objective is to position frequently changing code in the upper layers, while the more stable domain-related code resides in the lower layers.
By adhering to this thoughtful layering approach, we can easily change code or even replace top layers without causing any impact on the bottom layers.
Better Organization
Implementing a layered approach in our codebase is an essential practice that brings order and structure to our codebase.
We could write our entire code in a single file, but instead, we divide our code into multiple files to improve organization. These files are then logically grouped and organized into folders, reflecting a hierarchy.
Similarly, taking it a step further, we introduce the concept of layers, partitioning of our codebase into distinct functional units. Each layer serves a specific purpose, encapsulating related functionalities within its boundaries. This architectural pattern promotes modularity and clarity, as we can easily locate and comprehend the relevant code for each layer.
Dependency Rule
The other cornerstone of Clean Architecture is the Dependency Rule, a fundamental principle that guides the structure of our codebase.
Simply put, the Dependency Rule emphasizes that source code dependencies should always flow in a downward direction. Each layer within the architecture must rely on the layer directly below it for its functionalities and operations, while remaining completely unaware of the layers above it. This segregation ensures a decoupled design.
In practical terms, this means that any component in a given layer should not reference or have knowledge of any components residing in the layers above it. The information exchange between layers should occur solely through data formats or interfaces defined by the lower layer. This gives us the flexbility to easily swap out above layers without affecting the code in lower layers.
In the image above, this type of dependency rule is strict where each layer only talks to the layer directly beneath it. There's an another more relaxed version of it where a layer can to talk to any layer below it. Try to avoid the relaxed version as it creates strong coupling between layers.
Implementation of Clean Architecture in ASP.NET
You can checkout Steve Smith's or Jason Taylor's implementation of Clean Architecture.
Being Pragmatic
While being pragmatic, if we only ever going to have a single Execution Layer such as WebApi then we can remove the Application Layer.
The benefit of having an Application layer is code reusability as multiple Execution Layers can use the same code. But most of time we usually have only one Execution Layer.
Benefit of removing the Application Layer is that we are getting rid of unnecessary abstraction. Second, in case of ASP.NET applications we can use inbuilt features to handle cross-cutting concerns such as Authorization, Validation etc.
To remove, move contracts to Domain Layer and rest of the components to the Execution Layer.
Conclusion
In conclusion, Clean Architecture, distilling various architectural paradigms like Hexagonal and Onion architecture into a coherent and powerful concept. Its essence revolves around two fundamental principles: Layers and the Dependency Rule.
Layers serve as the backbone of Clean Architecture, providing a strategic organization of related functionalities within the application. While the Dependency Rule, enforces a strict downward flow of dependencies between layers.
Embracing Clean Architecture guides us towards thoughtfully crafted architectural design, which helps us keep our codebase maintainable and flexible at the same time.