NDepend Blog

Improve your .NET code quality with NDepend

Clean Architecture in ASP.NET Core

April 16, 2024 9 minutes read

The Clean Architecture pattern has gained significant popularity for the design and development of software applications. It emphasizes key principles to better maintain, scale, and test solutions thanks to well-defined layers and clear separation of concerns. Clean Architecture promotes abstracting external dependencies like databases, UI, or services to let the developer focus on the core domain code.

Understanding Clean Architecture

In this post, we’ll explore  Jason Taylor’s CleanArchitecture .NET solution template available here on github recently updated to support .NET 8. It illustrates well how an ASP.NET Core application can benefit from Clean Architecture principles.

The Onion Diagram of Clean Architecture

This Clean Architecture can be explained through an onion-architecture diagram: one concentric circle for each .NET project. We refined this diagram with the responsibilities and roles of each project:

Clean-Architecture-Diagram-Asp-Net

Understanding the Layers of the Onion Diagram

We can see that:

  • Dependencies flow toward the innermost circle which is the Domain. Clean Architecture is inspired by Domain Driven Design (DDD). The Domain project only depends on .NET CLR primitive types (int, string, Exception…). Application aspects like persistence, business rule, network access… are totally absent from the Domain. This way efforts can be put into the Domain modelization within being cluttered by those aspects.
  • This sample application aims at dealing with the creation, edition, and persistence of some TODO items and lists. Hence the TodoItem and TodoList classes in the Domain, that are actually entities in DDD terminology.
  • The Application project defines business rules based on the Domain’s entities and value objects. It also contains CQRS queries and commands and some interfaces that are implemented in the Infrastructure project.
  • The Infrastructure contains implementations of infrastructure interfaces defined in the Application project. This separation makes it convenient to change any infrastructure implementation at any time.
  • The WebUI contains Controllers and Views. It depends on the Infrastructure to inject Infrastructure’s implementations within the Application project at startup time. This bootstrap approach makes it easy to maintain and refactor. Notice the difference with the traditional 3 layers (UI, Business, Data) where everything ends up depending on the Data layer making it complicated to refactor and difficult to unit test.

Here is the Visual Studio Solution Explorer view of this Clean Architecture solution. Application code and test code are explicitly separated through a src and tests solution folder.

Clean Architecture Visual Studio Solution Explorer

Benefits of Clean Architecture

  • Domain-Centric: Clean Architecture is a design approach that centers around the domain and application layers.
  • Maintainability: In contrast to the conventional “N-Layer” architecture, this approach doesn’t depend on external factors such as the database, UI layer, or framework. The core of the application resides in the domain and application layers, free from the clutter of infrastructure details, thanks to the inverted dependency. This enhancement significantly improves maintainability.
  • Infrastructure Independence: Unlike Oracle or SQL Server, business rules are not tied to a particular database. You have the flexibility to employ various databases, including MongoDB, BigTable, CouchDB, and more. Another consequence is that the infrastructure implementation details can be changed without affecting the application core.
  • Testability: A consequence is that Clean Architecture allows for easy testing of business rules without relying on external elements like UI, web servers, or databases. This means that it is possible to verify business rules in isolation, without any other dependencies.
  • Streamlined Collaboration: A meticulously structured project simplifies concurrent collaboration for multiple developers.

Dependency Graph View of a Clean Architecture

The Clean Architecture Onion Diagram above is a logical view that took time to be drawn manually. You can obtain automatically a physical view of this solution through the NDepend dependency graph: it details classes and namespaces within projects and also shows compile-time dependencies.

Notice the color scheme: Orange means selected, green means caller of selected, and blue means called by selected. Further screenshots will rely on this color scheme.

Clean Architecture Visual Studio NDepend Graph

Coupling and Stability in Clean Architecture

A significant takeaway from this dependency graph is that Clean Architecture is all about the coupling of types and the direction of dependency between them. For example, it is recommended that Domain classes only depend on .NET primitive types,. On the other hand only bootstrapping code depends on Infrastructure implementations. But why such controlled coupling is desirable? Because of stability!

Stability is a crucial aspect of Clean Architecture. The Domain project’s lack of dependencies ensures that its business logic remains isolated. Changes in other layers like Infrastructure do not affect it. On the other hand, we accept that application changes may provoke changes in WebUI. But we abstract Infrastructure through interfaces because a change in Infrastructure shouldn’t provoke any changes in other layers. By prioritizing stability through proper dependency management, we can ensure a robust and maintainable system.

To better explain Clean Architecture, it is often divided into four projects named after each layer. However, this division is only a convenient artifact for illustrative purposes. In reality, larger solutions may require multiple projects within each layer, while smaller solutions may merge certain layers into a single project. The important thing is to maintain a clear separation of concerns and a proper dependency hierarchy within the system.

Now that we have a clear understanding of the Clean Architecture pattern and its benefits, let’s explore each layer.

Domain

As the CleanArchitecture‘s github page says, the domain project contains all entities, enums, exceptions, interfaces, types, and logic specific to the domain layer.

Clean Architecture and Domain Driven Design

In software development, the domain is critical in establishing a shared language and abstractions throughout an application and organization. This means that all team members must use consistent terminology, such as Client or (exclusive) Customer to avoid ambiguity. The concept behind this approach is known as Ubiquitous Language in Domain-Driven Design (DDD). To ensure consistency in terminology, NDepend offers a customizable DDD ubiquitous language check that uses a dictionary of terms to validate domain terminology.

DDD includes several other key concepts, including Entity and Value Object. Typically, you implement these concepts in C# using classes and structures/enumerations. It is because there are similarities between entity and class, and value object and structure/enumeration:

  • An entity is defined by its identity the same way an object – an instance of a class at runtime – can be identified through obj.GetHashCode().
  • The identity of a value, like a C# enumerations value or a C# structure instance, is defined by the values of its fields. This is the reason why it is recommended to make a structure immutable. Interestingly Jason Taylor took another approach and implemented its value object Colour through a class that behaves like a structure because it derives from the special class ValueObject that enforces by-value-comparison. This choice seems over-engineered and I asked Jason the reason why. He answered it is just for demonstration purposes. In real world an enumerations would have been sufficient to implement Colour. But a ValueObject derived class would have been required to implement something like a PostalAddress value type.

Clean Architecture and Plain Old CLR Objects (POCO)

One key fact to underline is that types inside the Domain project are Plain Old CLR Objects (POCO). To make that point clear, the graph below shows the .NET BCL types consumed by the project Domain. We can see that only primitive types and simple types like int, string, Exception, Lis<T>… are consumed by the domain. Other projects contain implementations of concerns (like storage or UI) and also business rules.

The Domain Project

Application

The Application project only depends on the Domain project. It contains business rules implementations. Some examples of such rules are:

  • Sending an email to a client upon some specific conditions
  • Validating user input
  • Calculating values like a discount on an order
  • Transforming data

By double-clicking the edge between the Application and the Domain projects, we get a graph of classes and methods involved in the coupling between the two projects. You can click on the image to enlarge it:

Coupling Between Application And Domain

The implementation of the Application project, with its distinct command and query classes, clearly utilizes the Command Query Responsibility Segregation (CQRS) pattern. This pattern distinctly separates creation and update operations (commands) from read operations (queries), with each type returning its own response models to ensure clarity and segregation of actions. The adoption of CQRS is particularly beneficial in complex domains where using a single model for both commands and queries can lead to an overly complicated system that fails to perform optimally in either role. Thus, implementing CQRS enhances the long-term maintainability of the application.

Apart from implementing the business rules, the Application project has a second responsibility: abstract away concerns. Concerns of the application include notification services, database access, SMTP, file system access…  For example, the interface IApplicationDbContext is used to abstract the database accesses (create/update/read) of TodoLists and TodoItem entities defined in the domain.

If we look at the UpdateTodoListCommand and its handler code, you can see how the Application consumes the database without any knowledge of its implementation.

Infrastructure

The Infrastructure project implements interfaces defined in the Application projects. We can see how it depends on external resources such as entity framework or identity provider and so on.

The Infrastructure Project Dependencies

Separating the infrastructure implementation from its usage in the Application project is one of the greatest benefits of this CleanArchitecture template. In the future, we’ll be able to change easily any of these concerns implementations. Here are these interfaces and implementations matched by a code query:

Clean Architecture Infrastructure-Interfaces

It may seem premature to abstract from all infrastructure concerns. However creating such interfaces allows for the mocking of these concerns in test code, thereby enhancing testability—a significant benefit in itself. Moreover, experience indicates that over time, the implementations of these concerns tend to evolve due to changes in the industry itself.

WebUI

One responsibility of the WebUI project is to inject the Infrastructure implementations within the Application layer. This is achieved by the Startup and Program classes that reside in the WebUI project. This is the only reason why the WebUI project references the Infrastructure project. This becomes clear by looking at the class coupling between the two projects:

WebUI usage of Infrastructure

Apart from that part, the WebUI project is a single-page application based on Angular 10 and ASP.NET Core 5. Nothing special but you can look here at how the WebUI controllers use the CQRS commands for example.

Test Organization

Tests focus on testing projects Application and Domain. Personally, I don’t necessarily segregate tests by projects tested like here. However, the distinction between Integration and Unit tests is necessary because they serve different purposes: whether you are working on a single class or on combined modules together you know which tests to run to prevent regressions.

Clean Architecture Visual Studio Solution Explorer

Additionally, unit tests run quickly, often at a rate of thousands per second, so developers don’t face a performance penalty from running them frequently. As long as tests don’t hinder a developer’s pace, they should be run as often as possible.

Conversely, integration tests tend to execute more slowly and are usually run daily on the build server, unless there is a specific need for developers to run them locally.

Finally, by running tests, I obtained 39% overall coverage. Covered and uncovered classes and methods can be seen on the metric view (screenshot below). I would have wished for more tests and coverage.

Clean Architecture Code Coverage

Critics Against Clean Architecture

A common critique of Clean Architecture is that it disperses each feature across multiple layers. For instance, classes related to TodoItem can be found in all four layers:

Clean-Architecture-Feature-Spawn-Across-All-Layers

This goes against code locality: the code related to similar functionality should be co-located: in one namespace or in one layer.

This is why another popular architecture pattern gets more and more popular: Vertical Slice Architecture. With this pattern we think in terms of feature and not in terms of layer. You can refer to this great 46mn video from Derek Comartin / CodeOpinion to know more about this pattern.

This approach conflicts with the principle of code locality, which suggests that code pertaining to similar functionalities should be grouped together, either within a single namespace or a single layer.

This is one reason why the Vertical Slice Architecture is gaining popularity. This pattern focuses on organizing code by feature rather than by layer, promoting more cohesive and intuitive code organization. You can refer to Vertical Slice Architecture in ASP.NET Core.

Conclusion

In our presentation, we explored the structure and interactions of each layer in Clean Architecture. By managing the coupling between types, Clean Architecture provides a framework for stabilizing the application’s core types. This focus on stability leads to a system that is more manageable and maintainable.

Given that Clean Architecture is increasingly becoming the norm, it is essential to understand its principles and implement them in your applications.

The origin of Clean Architecture comes from the principles developed by Robert C. Martin, also known as “Uncle Bob”. So let’s end up with a quote from the Uncle Bob’s Clean Architecture book page 137:

The primary purpose of architecture is to support the life cycle of the system. Good architecture makes the system easy to understand, easy to develop, easy to maintain, and easy to deploy. The ultimate goal is to minimize the lifetime cost of the system and to maximize programmer productivity.

Comments:

  1. Levi Fletcher says:

    Great article! I’ve use the product before and it really helped assess our code quality!

  2. > we only want the domain project to reference base assemblies like System.Runtime

    The way this is phrased is either misleading, or it sounds like a bad idea. For instance, if I’m only supposed to reference base assemblies like System.Runtime, I guess that means I can’t reuse any useful domain abstractions that someone else may have built, like for currencies, geolocation, or postal addresses, even when these are domain concerns.

    You’re suggesting that I must rebuild the needed data structures myself only from the primitive types defined in the base CLR assemblies, even if a perfectly good data structure that isn’t in the base assemblies already exists?

  3. Michael Ganzer says:

    You’re on a good way towards Clean Architecture. The solution template gives you a neat head start.

    You can still segregate the segments within the rings, keep them apart from each other, abstract your presenter and split the presenter coupling from the persistence gateway. There is still lots of convention left to the coder, so he does not to fall for an unwanted coupling. You could provide supportive structure very easily by splitting interfaces and DTOs from implementation projects.

    Furthermore you could benefit from using a runtime project with plain hosting capability, which itself then references a configuration project that knows all dependency injection.

    So far, good job!

  4. Martin Moe says:

    Very good article Patrick and Scott. Very good. Seeing the new goodies from later versions of NDepend than the one I am using I think I have to ask the guys with the purse for a license upgrade 😉

  5. Chatrughan says:

    Nice article describe

  6. CO Workers says:

    We don’t like one aspect of Jason Taylor’s CleanArchitecture .NET solution template very much. We agree with Uncle Bob here that the application layer should not depend on the Entity Framework.

  7. Your clean architecture is a bit dirty. There is no need of separating Infrastructure from Application level. No need of decopouling interfaces from business logic implementation. And no need of hiding implementation behind interfaces. Better is to have one Service layer between Application and Domain layers

  8. This is a great post on the implementation of Clean Architecture in an ASP.NET Core solution. The use of a case study makes the information relatable and easy to understand. Clean Architecture is a widely recognized approach to software design that emphasizes separation of concerns, modularity, and maintainability. By applying these principles to an ASP.NET Core solution, organizations can improve the overall quality and longevity of their software. Thank you for sharing this valuable information!

Leave a Reply

Your email address will not be published. Required fields are marked *