NDepend Blog

Improve your .NET code quality with NDepend

Clean Architecture in ASP.NET Core

October 20, 2023 9 minutes read

The Clean Architecture approach has gained significant popularity for the design and development of software applications, emphasizing key principles such as maintainability, testability, and the clear separation of concerns.

Clean Architecture is Robert C. Martin’s approach to designing software for long-term maintainability and scalability. It prioritizes creating a decoupled, modular architecture, enabling independent development, testing, and maintenance of applications, free from external dependencies like databases, UI, or services.

Understanding Clean Architecture

In this post we’ll explore the 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.

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

Clean-Architecture-Diagram-Asp-Net

We can see that:

  • Dependencies flow toward the innermost circle which is the Domain. Clean Architecture is inspired from Domain Driven Design (DDD). The Domain project only depend on .NET CLR primitive types (int, string, Exception…). Application’s aspects like persistence, business rule, network access… are totally absent from the Domain. This way efforts can be put into the Domain modelisation 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 class 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 only in order 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 folders.

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

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 show compile-time dependencies.

Notice the color scheme: Orange means selected, green means caller of selected and blue means called by selected. Further screenshot 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, while 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 and unchanged, unaffected by changes in other layers like Infrastructure. On the other hand, we accept that changes in application may provoke changes in WebUI. But we abstract Infrastructure through interfaces because a change in Infrastructure shouldn’t provoke any changes into other layer. 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 artefact 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 architecture and its benefits, it makes sense to explain 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.

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 DDD. To ensure consistency in terminology, NDepend offers a customizable DDD ubiquitous language check that uses a dictionary of terms to validate domain terminology.

Domain-Driven Design (DDD) includes several key concepts, including Entity and Value Object. These concepts are typically implemented 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 – instance of a class at runtime – can be identified through obj.GetHashCode().
  • The identity of a value, like a C# enumerations values 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.

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. Concerns (like storage) and also business rules are implemented in other projects.

The Domain Project

Application

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

  • Send this email to this client if these conditions are verified
  • Validate this user input
  • Calculate values such as a discount on an order.
  • Data transformation

By double-click 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

With commands and queries classes, it is clear that this Application project impl is relying on the Command Query Responsibility Segregation (CQRS) pattern. This pattern is used to separate create and update operations (commands) from reads (queries) each returning their response models for clear segregation of each action. The reason for using CQRS is that for a complex enough domain, relying on a single conceptual model for both commands and queries leads to a too complicated model that does neither well. Thus, the benefit of applying CQRS in such situation is that the long-term maintainability of the the application improves.

Apart 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 we can see how the database is consumed from the Application with zero knowledge about how it is implemented.

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

This might sound like premature to abstract away from all infrastructure concerns. However such interfaces could let mock concerns in tests code and this would be a great benefit of abstracting. Also experience shows that with time, concerns implementation change because the industry itself evolves.

WebUI

One responsibility of the WebUI project is to inject the Infrastructure implementations within the Application layer. This is done by the Startup and Program classes defined 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

You can write a custom code rule to make sure that Infrastructure is only used by the WebUI classes Main and Startup. This rule captures the initial CleanArchitecture intention and will warn upon any violation this intention.

Apart that cool 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 uses the CQRS commands for example.

Test Organization

Tests focuses on Application and Domain testing. 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 a 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

Also unit tests are fast to execute, typically in the range of thousands per second. Thus the developer doesn’t have a performance penalty when executing them often. And as long as tests don’t slow down the developer they should be executed as often as possible.

On the other hand integration tests can be slow to execute and are typically executed daily on the build server, except when the developer really needs to execute them locally.

Finally, by running test I obtain 39% overall coverage. Covered and uncovered classes and methods can be seen on the metric view. I would have wished more test and coverage.

Clean Architecture Code Coverage

Critics against Clean Architecture

There is a critic against Clean Architecture: each feature is spawn among each layer. For example There are classes related to TodoItem in all 4x 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.

Conclusion

In our presentation, we discussed the content and interactions of each Clean Architecture layer. By controlling the coupling between types, Clean Architecture offers a recipe for achieving stability for the application’s core types. This emphasis on stability ensures a more manageable and maintainable system overall.

This kind of clean architecture is becoming standard nowadays so better master its concepts an apply it in your applications.

To finish, let’s remind why caring for the architecture of your solution is important. Here is 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!

Comments are closed.