Implementing a Clean Architecture in ASP.NET Core 6
Introduction
This post is the first part in a series of posts which describe my personal take on implementing a Clean Architecture with ASP.NET Core 6, and hopefully give you some ideas on how to go about building your own clean architecture.
This part is merely an overview of the overall architecture. More details on the internals and implementation will follow in separate posts.
Also, this post will not describe the concepts of a clean architecture in detail, nor will be an introduction to Domain-Driven Design or best practices. If you need to learn more about clean architecture concepts in detail, you can follow one of the links on the bottom of this post.
The intention of this design is to act as a starting point for building ASP.NET Core solutions. It is loosely-coupled, relies heavily on Dependency Inversion principle, and promotes Domain-Driven Design for organizing your core (although this is not forced). The goal for which I set out to do this, was to build a bare-bone, maintainable and extendable architecture
Technologies used
- ASP.NET Core 6 (RC1 at the time of writing)
- Entity Framework Core 6 (RC1 at the time of writing)
- MassTransit
- AutoMapper
- Razor Components
- ASP.NET Core MVC
- GuardClauses
- xUnit
- Moq
- Fluent Assertions
- FakeItEasy
- Docker
Features
The features of this particular solution are summarized briefly below, in no particular order:
- Localization for multiple language support
- Event sourcing using Entity Framework Core and SQL Server as persistent storage, including snapshots and retroactive events
- EventStore repository and DataEntity generic repository. Persistence can be swapped between them, fine-grained to individual entities
- Persistent application configurations with optional encryption
- Data operation auditing built-in (for entities which are not using the EventStore)
- Local user management with ASP.NET Core Identity
- Clean separation of data entities and domain objects and mapping between them for persistence/retrieval using AutoMapper
- ASP.NET Core MVC with Razor Components used for presentation
- CQRS using handler abstractions to support MassTransit or MediatR with very little change
- Service bus abstractions to support message-broker solutions like MassTransit or MediatR (default implementation uses MassTransit’s mediator)
- Unforcefully promoting Domain-Driven Design with aggregates, entities and domain event abstractions.
- Lightweight authorization framework using ASP.NET Core AuthorizationHandler
- Docker containerization support for SQL Server and Web app
Some other goodies:
- Password generator implementation based on ASP.NET Core Identity password configuration
- Razor Class Library containing ready-made Blazor components for commonly used features such as CRUD buttons, toast functionality, modal components, Blazor Select2, DataTables integration and page loader
- Common library with various type extensions, result wrapper objects, paged result data structures, date format converter and more
Architecture Overview
Further below is a figure depicting the overall architecture in terms of layers and their components. The figure is meant to be a representation of how the actual solution is layered and show the gradual interrelationships between the layers, as opposed to simply showing the logical structure of a clean architecture.
In any case, the below diagram taken from Jason Taylor’s talk at NDC Sydney (2019), broadly depicts the logical structure of the architecture:
The actual solution consists of several projects, separated in folders representing the layers for convenience. The below figure is a code map generated from the solution. It shows the different layers and projects, along with their inter-dependencies.
As you can see from the arrows, all dependencies point downwards (or inwards if this was a circle diagram). there are no arrows pointing upwards between the Core, Infrastructure and Presentation layers.
The Core Layer
The Core Layer is made up of two parts, the inner core and outer core. The inner core is the domain and the outer core is the application. This consists of two projects in the solution under the Core folder, the Application and the Domain projects.
Inner Core (Domain Layer)
This layer contains application-independent business logic. This is the part of the business logic which would be the same even if we weren’t building a software. It is a formulation of the core business rules.
The organization of this project follows Domain-Driven design patterns, although this is a matter of preference and can be handled any way you see fit. This includes:
- Aggregates, entities, value objects, custom domain exceptions, and interfaces for domain services.
- Interfaces for domain-driven design concepts (i.e. IAggregateRoot, IDomainEvent, IEntity).
- Base implementations of aggregate root and domain event. Also contains specific domain events pertaining to the business processes.
Outer Core (Application Layer)
This layer contains application-specific business logic. This contains the “what” the system should do. This includes:
- Interfaces for infrastructure components such as repositories, unit-of-work and event sourcing.
- Commands and Queries models and handlers
- Interfaces and DTOs for cross-cutting concerns (i.e. service bus)
- Authorization operations, requirements and handlers implementations
- Interfaces and concrete implementations of application-specific business logic services.
- Mapping profiles between domain entities and CQRS models
The Infrastructure Layer
This layer contains details, concrete implementations for repositories, unit-of-work, event store, service bus implementations etc. This contains the “how” the system should do what is supposed to do. The decoupling between the application layer and the infrastructure layer is what allows solution structures like this one to change and/or add specific implementations as the project requirements change.
In overview, this layer contains:
- Generic and specific repositories implementations
- EF DbContexts, data models and migrations
- Event sourcing persistence and services implementations
- Implementations for cross-cutting concerns (i.e, application configuration service, localization service etc.)
- Data entity auditing implementation
This consists of 3 projects in the solution under the Infrastructure folder, the Auditing, Data and Shared projects.
The Auditing project consists of various extensions methods for DbContext, primarily related to SaveChanges. It is responsible for generating auditing records for each tracked entity’s changes.
The Data project contains domain and generic (CRUD and event-sourcing) repository implementations, DbContexts, EF Core migrations, entity type configurations (if any), event store implementation (including snapshots), data entity to domain object mappings, and persistence related services (i.e. a database initializer service).
The Resources project contains localized shared resources and resource keys, along with localization services implementations.
The Shared project contains service implementations for cross-cutting concerns such as user management and authentication, file storage, service bus, localization, application configuration and a password generator.
The Presentation Layer
This layer essentially contains the I/O components of the system, the GUI, REST APIs, mobile applications or console shells, and anything directly related to them. It is the starting point of our application.
For this starting point solution, it contains the following:
- ASP.NET Core MVC web application using Razor Components
- A shared class library containing common Razor Components, such as toast notifications, modal components, Blazor Select2, DataTablesJS integration and CRUD buttons
The Common Layer
This single class library contains common types used across all other layers. What you would typically include in this library is things that you would wrap up into a Nuget package and use in multiple solutions. For clarification, this does not represent the Shared Kernel strategic pattern in DDD. Some of these include:
- A generic Result wrapper
- Paged result models for query operations
- CLR type extensions
- Notification models
- Custom attributes and converters
Yes, yes, give me the code
The repository for the source code of this solution can be found here. The solution includes an extremely basic domain model for an online shop application, as an example. The UI part for this is not included however. The order domain exists to simply showcase the different elements of the architecture.
Final thoughts
Just like any software solution, this is not a one-size-fits-all architecture. It is simply what worked very well for me in my projects. As mentioned in the introduction of this post, there is still quite a lot to include in this solution, therefore I will be updating the repo on GitHub, as well as this post, whenever something new is added.
Please feel free to leave any feedback or suggestions on this, and if you would like to have anything in particular be included in the project.
Future work
In the near future, I’ve planned to make the following additions/changes to the solution:
- Add support for caching, with an implementation for Redis
- Replace MVC for presentation with Blazor Server
- Include a RESTful API for domain business logic
- Additional implementation of event-sourcing using EventStoreDB
Resources
Here are some additional posts you may find helpful in understanding what clean architecture is all about:
https://www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/