Clean Architecture in ASP.NET Core Web API

Clean Architecture in ASP.NET Core Web API

In this article, I will explain Clean Architecture in ASP.NET Core Web API with Examples. Please read our previous article discussing Core Microservices Design Principles. Clean Architecture is a software design philosophy emphasizing the separation of concerns, scalability, and maintainability. It helps in creating systems that are easier to understand, test, and extend over time. In this post, we will explain Clean Architecture in the context of an ASP.NET Core Web API project using SQL Server and EF Core Code-First approach, and cover its key principles and layers.

What Is Clean Architecture?

Clean Architecture was introduced by Robert C. Martin (Uncle Bob) to create a system that is flexible, maintainable, and testable by organizing the code into clearly defined layers. Each layer has its own responsibilities, and changes in one layer of the system do not affect other layers. Clean Architecture separates the system into layers, each with its own specific function. This helps in managing changes over time without affecting the core logic.

Example: Building a House

Imagine building a house:

  • Core Structure: The foundation and walls represent the core business logic. These parts are fundamental and should not change frequently.
  • Functional Elements: The paint, plumbing, furniture, and lighting are functional parts that can be easily changed or upgraded without affecting the core structure.

In Clean Architecture, the core structure of the house represents your business rules, and the functional elements (paint, plumbing, furniture) represent external systems like UI, databases, and external APIs. By keeping these parts separate, you can modify one part without changing the others.

Why is Clean Architecture Important?

Let’s understand why Clean Architecture is important through one real-time example. Imagine you run a bakery with machines for different tasks:

  • Bread-baking machine: Does the baking.
  • Dough-mixer: Mixes the dough.
  • Packaging machine: Handles packaging.

If all the machines are tightly connected (e.g., the dough-mixer is directly connected to the bread-baking machine), then when one machine breaks, it may affect all others. This can make fixing or upgrading individual machines difficult.

With Clean Architecture, each machine in the bakery works independently. If one machine breaks down, only that machine is affected, and the rest can continue working. You can also replace or upgrade any machine without disturbing the bakery’s overall operation.

Similarly, in software development, Clean Architecture provides:

  • Flexibility: You can change or swap parts of the system (e.g., replace SQL Server with MongoDB) without affecting others.
  • Maintainability: If one part needs improvement or a bug fix, you can change it without touching the entire system.
  • Scalability: You can easily add more features or support more users, thanks to the system being organized into independent, modular layers.
Key Principles of Clean Architecture

Clean Architecture follows basic principles to ensure the system remains organized and easily managed. These principles are like the rules in a game that help you play in an organized way. The following are the Key Principles of Clean Architecture:

Separation of Concerns:

Think of a library where books are organized into sections like fiction, non-fiction, science, and history. Each section has a specific purpose, and they don’t mix with each other. In software:

  • The UI (user interface) should focus only on displaying information.
  • The business logic should handle processing that information.
  • The database should be concerned with storing and retrieving data.

This separation makes it easier to update or modify one part of the system (like updating the UI) without affecting other parts (like the database or business logic).

Dependency Inversion Principle

Imagine you have a remote control that can control both a TV and an air conditioner. If you decide to replace your TV, you don’t need to change the remote control. The remote is independent of the TV or air conditioner.

In software, this principle means:

  • High-level modules (like business logic) should not depend on low-level modules (like database access).
  • Both should depend on abstractions (like interfaces or abstract classes). The high-level module should only care about what needs to be done, not how it’s done.

This allows flexibility. For example, you can replace SQL Server with MongoDB without modifying the business logic, because the business logic only depends on interfaces, not on how the data is stored.

Encapsulation and Abstraction

Imagine a washing machine: you don’t need to know how it washes clothes (the internals). You just press a button to start it. In software:

  • Encapsulation hides the internal complexity.
  • Abstraction provides a simple interface to interact with the system, exposing only what’s necessary.

This allows the system to interact with only the relevant parts and hide unnecessary complexity.

Layers of Clean Architecture

Imagine you are building a restaurant. A restaurant has different parts, each responsible for a specific task, and each part works independently to ensure everything runs smoothly. Each part of the restaurant can be considered one of the layers in Clean Architecture. Here’s how:

Presentation Layer (Waiter)

The waiter interacts with the customers (users), takes their orders, and delivers the food. The Presentation Layer handles the user interface (UI) or API requests.

  • In a Web App: It’s the buttons, forms, and views that the user interacts with.
  • In an API: It handles the HTTP requests (like GET or POST requests).
Application Layer (Chef)

The chef prepares the food based on the order the waiter brings. The Application Layer contains the business logic and use cases, which are the tasks the system can perform.

  • Example: For an e-commerce app, it checks if a product is in stock and processes the order.
Domain Layer (Menu)

The menu defines the dishes available in the restaurant. The Domain Layer contains the core business rules and entities, like Order, Customer, and Product.

  • Example: An Order entity may have methods to add items and calculate the total price.
Infrastructure Layer (Supply Chain)

The supply chain handles the raw materials and tools the kitchen needs to prepare the food. The Infrastructure Layer connects to external systems like databases, external services, and APIs.

  • Example: It may connect to a SQL Server database using EF Core to retrieve data or store information.
Understanding DTOs vs. Domain Models

In software design, DTOs (Data Transfer Objects) and Domain Models are two different concepts. Both are used to represent data, but they serve different purposes. Let’s understand these in a way that makes sense using a library analogy.

What is a Data Transfer Object (DTO)?

Real-World Example: The Library Catalogue (DTO): Imagine you want to borrow a book from a library. The library catalogue is a summary of all the books available, containing key details like:

  • Book title
  • Author
  • Genre
  • Availability (whether it’s available or checked out)

The catalogue doesn’t tell you everything about the book (e.g., the full text of the book, detailed chapters, or any behind-the-scenes info), but it gives you enough information to decide if you want to borrow it.

In software, a DTO is like this library catalogue. It’s a simple container used to transfer data between different parts of an application (or between different systems). A DTO contains only the necessary data for a particular operation or interaction, like sending a form to the user or receiving a request from the API.

For example, when a user requests information about their profile, the application might create a DTO that contains:

  • The user’s name
  • The user’s email
  • The user’s address

The DTO is used to transfer this specific data over the network or between layers in the application. A DTO is a lightweight object used only to carry data from one place to another.

Difference Between DTOs and Domain Models

Let’s compare DTOs and Domain Models by continuing with the library analogy.

Real-World Example: The Book (Domain Model): Now, imagine the book itself in the library. This is the actual book you want to borrow. The book contains:

  • The full text of the chapters
  • The author’s background
  • Detailed publishing info

This book is like the Domain Model in software. A Domain Model is an object that represents the real-world business concept in your application, with full details and logic. It doesn’t just contain data—it also includes the business rules and behaviours that define how the data behaves. In a library system, the Domain Model could be:

  • Book: With full attributes like the book’s content, publishing details, and the logic for whether a book can be checked out or not.

Difference:

  • DTO: A lightweight object containing only the data needed for a particular task, like showing the book title or author in a catalogue.
  • Domain Model: Represents the complete entity with business logic and behaviours, like the full content of the book and rules for borrowing it.
Why Use DTOs and When to Use Them:

Real-World Example: Communicating with the Customer. Imagine you are working at the library desk and a customer comes to ask for a book. You don’t hand them the entire book (the full Domain Model) because they’re just asking about availability, not the full content. Instead, you give them a brief catalogue (DTO) that tells them:

  • Book availability
  • Title
  • Author
Why Use DTOs:
  1. Efficiency: DTOs are used to transfer only the relevant data needed for the task, without overwhelming the system with unnecessary details.
  2. Network Optimization: When sending data over the network (e.g., from a server to a client), it’s more efficient to send only the necessary data rather than the entire domain object.
  3. Decoupling: Using DTOs helps decouple the internal system’s data structure (domain model) from what is shared with the outside world (e.g., the API). This means you can change your domain models without affecting how users or external systems interact with your application.
When to Use DTOs:
  • When sending data to the UI or external systems: For example, sending a user profile to a client-side app, but only showing the username, email, and profile picture, without the user’s sensitive data like password.
  • When you don’t need all the data from the Domain Model: Sometimes, you need just a subset of data (like showing a list of items), and a DTO simplifies the process by sending only that data.

Implementing a Simple Use Case – Student Management System using Clean Architecture

Let’s implement a Student Management System using Clean Architecture in an ASP.NET Core Web API application with SQL Server and EF Core Code-First approach. This system will be structured into four layers: Presentation, Application, Domain, and Infrastructure. Here is how each layer communicates with others:

  • Presentation Layer: Handles user interactions (HTTP requests via API controllers).
  • Application Layer: Coordinates business logic (services and DTOs).
  • Domain Layer: Contains the core business entities and business rules.
  • Infrastructure Layer: Implements data access (EF Core, repositories, and DbContext).
Step-by-Step Implementation

First, create a new ASP.NET Core Web API project named StudentManagement using the .NET 8 version. We will use Entity Framework Core with SQL Server, so we need to install the necessary packages. Please execute the following commands in Visual Studio Command Prompt.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Step 1: Create the Domain Layer
Role in the System:
  • The Domain Layer holds the core business logic and rules of the application. It is independent of external dependencies, such as databases, frameworks, or UI.
  • This layer contains entities (like Student) and interfaces (like IStudentRepository), which define the business rules and operations but don’t perform actual data access. It is agnostic of how data is stored or presented.
Key Components:
  • Entities: These represent the business objects, e.g., Student with properties such as Name, Email, and Age. Business rules are also encapsulated here (e.g., validation like ValidateAge()).
  • Interfaces: These define contracts for operations such as retrieving, adding, or updating data. For example, IStudentRepository specifies methods for interacting with Student data.
Example: Student Entity

The Student entity represents the core business logic. First, create a folder named Domain in the project root directory. Then, inside the Domain folder, create a class file named Student.cs and copy and paste the following code.

namespace StudentManagement.Domain
{
    // Domain Layer - Entity
    // Represents the core business logic and rules for Student
    public class Student
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Email { get; set; }
        public int Age { get; set; }

        // Business Rule: Ensure that the age is between 5 and 60
        public void ValidateAge()
        {
            if (Age < 5 || Age > 60)
            {
                throw new Exception("Age must be between 5 and 60.");
            }
        }
    }
}
Repository Interface: IStudentRepository:

The interface is a contract for data access that will be implemented in the Infrastructure Layer. Then, create a class file named IStudentRepository.cs within the Domain folder and copy and paste the following code.

namespace StudentManagement.Domain
{
    // Domain Layer - Interface
    // Defines operations to interact with student data (Implemented in the Infrastructure Layer)
    public interface IStudentRepository
    {
        Task<Student?> GetStudentByIdAsync(int id);
        Task<IEnumerable<Student>> GetAllStudentsAsync();
        Task AddStudentAsync(Student student);
        Task UpdateStudentAsync(Student student);
        Task DeleteStudentAsync(int id);
    }
}
Step 2: Create the Application Layer
Role in the System:
  • The Application Layer coordinates the use cases or business operations of the system. It acts as a mediator between the Domain Layer and the Presentation Layer.
  • This layer doesn’t contain business logic itself but manages the calls to the Domain Layer for business rule validation and data manipulation. It can also include DTOs (Data Transfer Objects) to structure data being passed between layers.
Key Components:
  • Service Classes: These classes define use cases or business operations (e.g., StudentService), implementing operations like adding, retrieving, or updating students.
  • DTOs (Data Transfer Objects): These are simplified objects used to transfer data between layers. They are useful for communication between the Application Layer and the Presentation Layer.
DTO: StudentDTO:

The DTO is used to transfer data between layers and the outside world. It is often used in the Presentation Layer to simplify the data sent to the client. First, create a folder named Application in the project root directory. Then, inside the Application folder, create a class file named StudentDTO.cs and copy and paste the following code.

namespace StudentManagement.Application
{
    // Application Layer - DTO
    // Represents data transferred between layers (e.g., Presentation Layer to Application Layer)
    public class StudentDTO
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Email { get; set; }
        public int Age { get; set; }
    }
}
Service: StudentService:

The StudentService is where the application’s business logic happens (e.g., creating, updating, and deleting students). So, create a class file named StudentService.cs within the Application folder and copy and paste the following code.

using StudentManagement.Domain;

namespace StudentManagement.Application
{
    // Application Layer - Service Class
    // Represents the business use cases and orchestrates calls to the Domain Layer
    public class StudentService
    {
        private readonly IStudentRepository _studentRepository;

        // Constructor Injection: Injects repository implementation from the Infrastructure Layer
        public StudentService(IStudentRepository studentRepository)
        {
            _studentRepository = studentRepository;
        }

        // Use Case: Fetch all students (Application Layer)
        public async Task<IEnumerable<StudentDTO>> GetAllStudentsAsync()
        {
            // Domain Layer: Fetches data via repository (Infrastructure Layer)
            var students = await _studentRepository.GetAllStudentsAsync();

            // Convert Domain Models to DTOs (Application Layer)
            return students.Select(s => new StudentDTO
            {
                Id = s.Id,
                Name = s.Name,
                Email = s.Email,
                Age = s.Age
            }).ToList();
        }

        // Use Case: Fetch a single student by ID (Application Layer)
        public async Task<StudentDTO?> GetStudentByIdAsync(int id)
        {
            // Domain Layer: Fetches data via repository (Infrastructure Layer)
            var student = await _studentRepository.GetStudentByIdAsync(id);

            if (student == null)
                return null;

            // Convert Domain Model to DTO (Application Layer)
            return new StudentDTO
            {
                Id = student.Id,
                Name = student.Name,
                Email = student.Email,
                Age = student.Age
            };
        }

        // Use Case: Add a new student (Application Layer)
        public async Task AddStudentAsync(StudentDTO studentDto)
        {
            // Convert DTO to Domain Model (Application Layer)
            var student = new Student
            {
                Name = studentDto.Name,
                Email = studentDto.Email,
                Age = studentDto.Age
            };

            // Apply business rule (Domain Layer)
            student.ValidateAge();

            // Domain Layer: Save the student (Infrastructure Layer)
            await _studentRepository.AddStudentAsync(student);
        }

        // Use Case: Update student (Application Layer)
        public async Task UpdateStudentAsync(int id, StudentDTO studentDto)
        {
            // Domain Layer: Fetch existing student
            var student = await _studentRepository.GetStudentByIdAsync(id);
            if (student == null)
                throw new Exception("Student not found");

            // Update properties based on DTO (Application Layer)
            student.Name = studentDto.Name;
            student.Email = studentDto.Email;
            student.Age = studentDto.Age;

            // Apply business rule (Domain Layer)
            student.ValidateAge();

            // Domain Layer: Update student via repository (Infrastructure Layer)
            await _studentRepository.UpdateStudentAsync(student);
        }

        // Use Case: Delete student (Application Layer)
        public async Task DeleteStudentAsync(int id)
        {
            // Domain Layer: Delete student via repository (Infrastructure Layer)
            await _studentRepository.DeleteStudentAsync(id);
        }
    }
}
Step 3: Create the Infrastructure Layer
Role in the System:
  • The Infrastructure Layer is responsible for handling external dependencies such as databases, file systems, external APIs, etc.
  • This layer contains the EF Core implementations of repositories and the DbContext class. It implements the interfaces defined in the Domain Layer (e.g., IStudentRepository) and provides concrete data access operations.
Key Components:
  • DbContext: This is part of EF Core and is used to interact with the database. It contains DbSet properties for each entity (e.g., Student).
  • Repository Implementations: The actual implementations of the repository interfaces (e.g., StudentRepository), which handle CRUD operations.
DbContext: ApplicationDbContext:

The ApplicationDbContext is the EF Core DbContext used to interact with the database. First, create a folder named Infrastructure in the project root directory. Then, inside the Infrastructure folder, create a class file named ApplicationDbContext.cs and copy and paste the following code.

using Microsoft.EntityFrameworkCore;
using StudentManagement.Domain;

namespace StudentManagement.Infrastructure
{
    // Infrastructure Layer - DbContext
    // ApplicationDbContext is responsible for database interactions via EF Core
    // Defines DbSets for entities and configures relationships.
    public class ApplicationDbContext : DbContext
    {
        // Constructor: DbContextOptions are passed in to configure the context for EF Core.
        // This constructor is used to configure the database connection string and options.
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        // OnModelCreating is used to configure tables, relationships, and seed data.
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Seed data for the Students table.
            modelBuilder.Entity<Student>().HasData(
                new Student { Id = 1, Name = "John Doe", Email = "john.doe@example.com", Age = 18 },
                new Student { Id = 2, Name = "Jane Smith", Email = "jane.smith@example.com", Age = 19 }
            );
        }

        // DbSet<Student> represents the "Students" table in the database.
        public DbSet<Student> Students { get; set; }
    }
}
Repository: StudentRepository:

The StudentRepository implements the IStudentRepository interface and uses EF Core for data access. So, create a class file named StudentRepository.cs within the Infrastructure folder and copy and paste the following code.

using StudentManagement.Domain;
using Microsoft.EntityFrameworkCore;
namespace StudentManagement.Infrastructure
{
    // Infrastructure Layer - Repository Implementation
    // Implements data access logic via EF Core for Student data.
    public class StudentRepository : IStudentRepository
    {
        private readonly ApplicationDbContext _context;

        // Constructor Injection: Injects ApplicationDbContext for data access.
        public StudentRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        // Fetch student by ID (Infrastructure Layer)
        public async Task<Student?> GetStudentByIdAsync(int id)
        {
            return await _context.Students.FindAsync(id);
        }

        // Fetch all students (Infrastructure Layer)
        public async Task<IEnumerable<Student>> GetAllStudentsAsync()
        {
            return await _context.Students.ToListAsync();
        }

        // Add new student (Infrastructure Layer)
        public async Task AddStudentAsync(Student student)
        {
            await _context.Students.AddAsync(student);
            await _context.SaveChangesAsync();
        }

        // Update existing student (Infrastructure Layer)
        public async Task UpdateStudentAsync(Student student)
        {
            _context.Students.Update(student);
            await _context.SaveChangesAsync();
        }

        // Delete student (Infrastructure Layer)
        public async Task DeleteStudentAsync(int id)
        {
            var student = await _context.Students.FindAsync(id);
            if (student != null)
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
            }
        }
    }
}
Step 4: Create the Presentation Layer

The Presentation Layer is responsible for handling user interaction, specifically HTTP requests. In a web API, it is the layer that processes incoming HTTP requests and sends back appropriate responses.

Role in the System:
  • It exposes the application’s use cases (from the Application Layer) through API controllers.
  • It maps incoming HTTP requests to DTOs, passes them to the Application Layer, and returns the results.
Controller: StudentController:

The StudentController handles HTTP requests and interacts with the Application Layer (i.e., the StudentService). So, create an empty API Controller named StudentController within the Controllers folder and copy and paste the following code.

using Microsoft.AspNetCore.Mvc;
using StudentManagement.Application;

namespace StudentManagement.Controllers
{
    // Presentation Layer - API Controller
    // Handles HTTP requests and responses for students.
    [Route("api/[controller]")]
    [ApiController]
    public class StudentController : ControllerBase
    {
        private readonly StudentService _studentService;

        // Constructor Injection: StudentService from the Application Layer
        public StudentController(StudentService studentService)
        {
            _studentService = studentService;
        }

        // GET: api/students
        [HttpGet]
        public async Task<ActionResult<IEnumerable<StudentDTO>>> GetAllStudents()
        {
            // Application Layer: Calls the service to fetch data
            var students = await _studentService.GetAllStudentsAsync();
            return Ok(students);
        }

        // GET: api/students/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<StudentDTO>> GetStudent(int id)
        {
            // Application Layer: Calls the service to fetch data
            var student = await _studentService.GetStudentByIdAsync(id);
            if (student == null)
                return NotFound();
            return Ok(student);
        }

        // POST: api/students
        [HttpPost]
        public async Task<ActionResult> AddStudent([FromBody] StudentDTO studentDto)
        {
            // Application Layer: Calls the service to add a new student
            await _studentService.AddStudentAsync(studentDto);
            return CreatedAtAction(nameof(GetStudent), new { id = studentDto.Id }, studentDto);
        }

        // PUT: api/students/{id}
        [HttpPut("{id}")]
        public async Task<ActionResult> UpdateStudent(int id, [FromBody] StudentDTO studentDto)
        {
            // Application Layer: Calls the service to update an existing student
            await _studentService.UpdateStudentAsync(id, studentDto);
            return NoContent();
        }

        // DELETE: api/students/{id}
        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteStudent(int id)
        {
            // Application Layer: Calls the service to delete a student
            await _studentService.DeleteStudentAsync(id);
            return NoContent();
        }
    }
}
Communication Flow Between Layers:

We have implemented the example using Clean Architecture in ASP.NET Core Web API. The following diagram represents the communication flow between the layers.

Clean Architecture in ASP.NET Core Web API

Here’s a breakdown:
  • Presentation Layer (API Controllers): Handles HTTP requests and passes data to the Application Layer for processing.
  • Application Layer (DTOs and Services): Coordinates business operations by interacting with the Domain Layer to validate and manipulate business data.
  • Domain Layer (Entities and Interfaces): This layer contains the core business rules and interfaces but does not interact with external systems directly.
  • Infrastructure Layer (EF Core and Repositories): This layer implements data access logic using EF Core, providing concrete implementations for interfaces defined in the Domain Layer.

By following Clean Architecture in ASP.NET Core Web API, we ensure modularity, testability, and maintainability, with each layer focusing on a specific responsibility.

appsettings.json File

This file contains the application settings, including the database connection string. So, please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=StudentsDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Configuring Dependency Injection:

The Program.cs class file configures the application’s services, including the EF Core DbContext and StudentService, and configures Swagger for API documentation. So, please modify the Program class as follows:

using Microsoft.EntityFrameworkCore;
using StudentManagement.Application;
using StudentManagement.Domain;
using StudentManagement.Infrastructure;

namespace StudentManagement
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
            // Optionally, configure JSON options or other formatter settings
            .AddJsonOptions(options =>
            {
                // Configure JSON serializer settings to keep the Original names in serialization and deserialization
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Add DbContext with SQL Server
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            // Configure Swagger (for API documentation)
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Register application services
            builder.Services.AddScoped<IStudentRepository, StudentRepository>();
            builder.Services.AddScoped<StudentService>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Creating and Applying Database Migration:

In Visual Studio, open the Package Manager Console and execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the StudentsDB database and the required Students table:

Creating and Applying Database Migration

Once you execute the above commands and verify the database, you should see the StudentsDB database with the required Students table, as shown in the image below.

Creating and Applying Database Migration

Final Project Structure using Clean Architecture:

Project Structure using Clean Architecture

Benefits of Adopting to Clean Architecture:
Scalability and Maintainability:
  • Scalability: Clean Architecture allows for easier system scaling. Because the system is broken into independent layers, you can scale specific parts without affecting the entire system.
  • Maintainability: With modular layers, changing one part of the system (like upgrading the database or adding new features) becomes less risky. You can improve in one layer, and the rest of the system remains unaffected.
  • Example: Imagine you start with a basic catalog system for an e-commerce application. Over time, you might need to add inventory management, customer reviews, or payment gateways. Since Clean Architecture keeps each feature isolated in its own layer, you can introduce these new features in the Application Layer or add a new database layer without disturbing the rest of the system.
Testability:
  • Independent Layers: Because each layer is isolated from the others, it’s easier to test individual components in isolation. Business logic can be tested independently of the database or external services.
  • Mocking: Testing doesn’t require a live database or external systems. For instance, you can use mocking to simulate database operations, allowing you to focus on testing only the core business logic.
  • Example: When testing the StudentService, you can mock the IStudentRepository (which interacts with the database). This allows you to test the logic for adding a student, for example, without needing a real database, ensuring the logic is correct before integrating with external systems.
Flexibility:
  • Swap Parts Without Impact: Clean Architecture allows you to replace or upgrade specific parts of the system without disrupting other layers. For example, you can switch out UI frameworks or databases without changing the core business logic.
  • Example: Suppose you replace the React front-end with Blazor or Angular. With Clean Architecture, this change can be made by only adjusting the Presentation Layer, leaving the Domain Layer and Application Layer untouched. This way, the system’s core logic remains intact while the front-end is swapped out.
Facilitating Easy Upgrades:
  • Adapting to New Technologies: As new technologies emerge, Clean Architecture helps integrate them smoothly with minimal changes. The modular structure makes it easy to replace or upgrade individual components without disturbing other parts of the system.
  • Example: If you need to switch from SQL Server to MongoDB, Clean Architecture allows you to make this change in the Infrastructure Layer alone. No changes are required in the Application Layer or Domain Layer, ensuring a seamless transition to the new database.
Understand the Clean Architecture with our Example:

Please have a look at the following diagram. The following diagram represents Clean Architecture, a software design pattern proposed by Robert C. Martin (Uncle Bob). It organizes the system into concentric layers, each serving a specific role, with the core business logic in the center.

Clean Architecture in ASP.NET Core Web API with Examples

Entities (Domain Layer)
Responsibilities:
  • This layer contains the core business logic and rules. It represents the fundamental principles that are independent of any external technologies (e.g., databases, frameworks).
  • The Domain Layer holds entities that represent the core business objects, such as Student, Order, or Product. It also contains the business rules and any other entities needed to model the core aspects of the problem domain.
Key Components:
  • Entities: These are business objects that are central to the domain, containing the business rules and logic.
  • Interfaces: These are abstractions, such as repositories (e.g., IStudentRepository), that define the operations to interact with the data storage or other external systems but do not contain the actual implementation.
Example:
  • The Student entity in the Domain Layer has properties like Name, Age, Email and contains logic like ValidateAge() to enforce business rules.

Purpose: The Domain Layer is independent of external systems, which ensures that business logic remains consistent and unaffected by changes in infrastructure (like changing a database from SQL to NoSQL).

Use Cases (Application Layer)
Responsibilities:
  • The Application Layer contains the use cases or application business rules. These rules define what the system can do and are the tasks or actions that the application supports.
  • This layer coordinates between the Domain Layer (which contains core business logic, Entities, and Interfaces) and the Presentation Layer (which handles user interaction, Controllers).
Key Components:
  • Services: These handle use cases by calling appropriate methods in the Domain Layer. They are also responsible for transforming data between layers and validating input.
  • DTOs (Data Transfer Objects): These are simple data containers used for transferring data between layers. They contain only the necessary data for a particular operation (e.g., StudentDTO with Name, Age, etc.).
Example:
  • In the Student Management System, the StudentService handles operations like adding, updating, or retrieving students and ensures that business rules are followed by interacting with the Domain Layer.

Purpose: The Application Layer focuses on the use cases and ensures that the correct operations are performed by coordinating tasks between the core business logic and external systems.

Interface Adapters (Presentation & Infrastructure Layer)
Responsibilities:
  • The Interface Adapters Layer is responsible for adapting the data between the internal application layers (such as Application and Domain) and the external systems (such as the UI, Database, or External APIs).
  • This layer contains the Presentation Layer and Infrastructure Layer, which both interact with external systems but follow the interfaces defined in the Domain Layer.
Key Components:
  • Presentation Layer (API Controllers): This part of the Interface Adapters Layer handles user interactions. For example, in a web application, it would handle HTTP requests, convert them into application-specific commands (like DTOs), and pass them to the Application Layer.
  • Infrastructure Layer (Repositories): This part is responsible for handling interactions with external systems (such as databases, external APIs, etc.). The Infrastructure Layer implements the interfaces defined in the Domain Layer, like repositories, but it doesn’t contain any business logic.
Example:
  • The StudentController in the Presentation Layer receives HTTP requests, calls the StudentService from the Application Layer, and returns the response. Similarly, the StudentRepository in the Infrastructure Layer implements the IStudentRepository interface and interacts with EF Core for data access.

Purpose: The Interface Adapters Layer ensures that external systems (like user interfaces or databases) can communicate with the internal application logic without directly coupling them to the core business logic.

Frameworks & Drivers Layer (External Systems)
Responsibilities:
  • The Frameworks & Drivers Layer consists of external systems and tools that support the application but don’t define the application’s business logic. It contains frameworks, third-party services, and technologies that interact with the application but don’t contain or influence its core behavior.
Key Components:
  • Databases: Any persistence mechanism like SQL Server, MongoDB, or SQLite. The Frameworks & Drivers Layer includes all database-related technologies, such as EF Core for relational databases or MongoDB drivers for NoSQL.
  • External APIs: Integrations with other services or third-party providers.
  • Web Frameworks: Tools like ASP.NET Core, React, Angular, etc.
Example:
  • EF Core or other ORM frameworks are part of this layer because they provide the tools to interact with the database, but they do not define the application’s business rules.

Purpose: The Frameworks & Drivers Layer interacts with the Infrastructure Layer and provides external capabilities such as data storage, user interfaces, or other services. It should have no dependencies on the core business logic.

Flow of Control (Following the Dependency Rule):
  • Presentation Layer: This layer interacts with the Application Layer to perform use cases. For web apps, it maps HTTP requests to DTOs and passes them to the Application Layer (e.g., API controllers).
  • Application Layer: Coordinates the business logic and interacts with the Domain Layer to process tasks (e.g., fetching data, performing business validation). It does not handle any direct interaction with external systems but coordinates between the Domain Layer and Presentation Layer.
  • Domain Layer: This layer contains the core business logic, rules, and entities. It defines interfaces (repositories) that the Infrastructure Layer implements. It is independent of any external systems or technologies.
  • Infrastructure Layer: This layer implements the interfaces defined in the Domain Layer to interact with external systems (e.g., databases, APIs). It does not contain business logic; it is only responsible for data access or external system integration.
  • Frameworks & Drivers: External technologies like databases, web frameworks, and third-party services that are used by the application but don’t influence the business logic.

Each layer in Clean Architecture has a clear, distinct responsibility. The Domain Layer holds the core business logic, while the Application Layer coordinates business use cases. The Interface Adapters Layer ensures communication between internal and external systems, and the Frameworks & Drivers Layer contains external tools and services that interact with the core business logic. By following this structure, we can easily change one part of the system (e.g., switch from SQL Server to MongoDB) without affecting other parts, making the system more modulartestable, and flexible for future growth and changes.

In the next article, I will discuss Domain-Driven Design (DDD) Principles with Examples. In this article, I explain Clean Architecture in ASP.NET Core Web API. I hope you enjoy this article, Clean Architecture in ASP.NET Core Web API with Examples.

1 thought on “Clean Architecture in ASP.NET Core Web API”

Leave a Reply

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