Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close
    Deep Dive.NETPart 1 of 12

    Clean Architecture: Introduction to the Project Structure

    A working Clean Architecture setup in .NET, explained through a real Contact Management API — layer responsibilities, dependency flow, project structure.

    Most Clean Architecture articles stop at the circle diagram. The part that actually decides whether a team can live with the pattern is more mundane: which project does this class go in, which direction do the references point, and where do the interfaces live. I built the Contact Management Application as a .NET Core Web API to answer exactly those questions with running code, and this series walks through it layer by layer.

    This first post is the map. It covers why the layering is worth the ceremony, what each layer owns, and how the solution is laid out on disk.

    1. Why use Clean Architecture?

    The benefits, stated once so I don’t have to repeat them in every section:

    • Maintainability: each layer can be modified independently; a UI or database change doesn’t touch core logic.
    • Testability: business logic decoupled from external dependencies is plain to unit test.
    • Scalability: clear boundaries let you swap a database or add features without a rewrite.
    • Framework independence: the core logic doesn’t care whether it’s hosted in ASP.NET Core or something else.

    The cost is more projects, more interfaces, and more mapping code. For a small CRUD app that trade can go against you; for anything that will be maintained by more than one person for more than one year, I take the trade.

    2. What is Clean Architecture?

    Clean Architecture divides an application into concentric layers, each with distinct responsibilities, and each layer only depends on the one directly inside it. Business logic sits at the center. The typical layers are:

    1. Domain Layer (Core Business Logic): business rules and core entities.

    2. Application Layer (Use Cases): orchestrates workflows and application logic, handling the interaction between the domain and external systems.

    3. Infrastructure Layer (External Systems): manages integrations with databases, file storage, or messaging systems.

    4. Presentation Layer (UI/Endpoints): interfaces directly with the user or API consumers.

    This keeps your core business rules isolated, so you can update external systems without disrupting internal workflows.

    The Dependency Inversion Principle (DIP) in Clean Architecture

    DIP is what makes the layering hold. It states that:

    • High-level modules (e.g., Application Layer) should not depend on low-level modules (e.g., Infrastructure Layer). Both should depend on abstractions.

    • Abstractions should not depend on details. The details (e.g., Infrastructure implementations) depend on abstractions (e.g., interfaces).

    In this codebase that plays out concretely: the Domain and Application Layers define interfaces like IGenericRepository<T>, and the Infrastructure Layer supplies the Dapper-backed implementations. Dependencies flow toward the center, so the Domain Layer never notices when an external system changes.

    3. Project structure (Clean Architecture in the Contact Management Application)

    The solution is organized into five projects, each owning one responsibility:

    src
    ├── Contact.Api // API Layer (RESTful APIs)
    ├── Contact.Application // Application Layer (business use cases, services)
    ├── Contact.Domain // Domain Layer (business logic, entities)
    ├── Contact.Infrastructure // Infrastructure Layer (persistence, external systems)
    └── Contact.Common // Shared utilities

    3.1 Domain Layer (Core Business Logic)

    • Purpose: the core of the application: business logic, rules, and entities that define what the application is actually for.

    • Contents: business entities, value objects, and domain services that enforce rules, validations, and calculations.

    • Independence: isolated from all other layers and external dependencies, so it stays stable when databases or APIs are updated or replaced.

    3.2 Application Layer (Use Cases)

    • Purpose: the intermediary between the Domain Layer and external systems; defines workflows and coordinates Domain Layer components to fulfill specific use cases.

    • Contents: application services, use cases, and data transfer objects (DTOs) that organize the flow of data between scenarios.

    • Role with Domain Layer: interacts with domain entities and services but does not alter core business logic. If a business rule changes, that change lands in the Domain Layer, not here.

    3.3 Infrastructure Layer (External Systems)

    • Purpose: manages interactions with external systems: databases, file storage, logging, and third-party services.

    • Contents: repositories, logging mechanisms, data access implementations (e.g., Entity Framework or Dapper), and third-party integrations.

    • Interaction with Application Layer: fulfills interfaces and abstractions defined by the Domain and Application Layers. It contains no business logic; it stores, retrieves, and processes data on behalf of the inner layers.

    3.4 Presentation Layer (UI/Endpoints)

    • Purpose: the system’s entry point, such as APIs in a backend service or user interfaces in a frontend. It gathers input from users or external systems and directs it to the Application Layer.

    • Contents: controllers, view models, UI components, and other elements that handle interactions with users or client systems.

    • Interaction with Application Layer: communicates exclusively with the Application Layer, which coordinates with the Domain and Infrastructure Layers to fulfill requests.

    4. Layer breakdown

    4.1. API Layer (Contact.Api)

    The API Layer is the entry point for the system, handling incoming HTTP requests. It maps the requests to services in the Application Layer and returns appropriate responses. In this layer, DTOs (Data Transfer Objects) abstract the underlying domain entities, keeping the client and the core domain separate.

    Example: ContactPersonController

    [ApiController]
    [Route("api/[controller]")]
    [Authorize]
    public class ContactPersonController : ControllerBase
    {
    private readonly IContactPersonService _contactPersonService;
    public ContactPersonController(IContactPersonService contactPersonService)
    {
    _contactPersonService = contactPersonService;
    }
    [HttpPost]
    [ActivityLog("Creating new Contact")]
    [AuthorizePermission("Contacts.Create")]
    public async Task<IActionResult> Add(CreateContactPerson createContactPerson)
    {
    var createdContactPerson = await _contactPersonService.Add(createContactPerson);
    return CreatedAtAction(nameof(GetById), new { id = createdContactPerson.Id }, createdContactPerson);
    }
    [HttpGet("{id}")]
    [ActivityLog("Reading Contact By id")]
    [AuthorizePermission("Contacts.Read")]
    public async Task<IActionResult> GetById(Guid id)
    {
    var contactPerson = await _contactPersonService.FindByID(id);
    if (contactPerson == null) return NotFound();
    return Ok(contactPerson);
    }
    // Other actions...
    }

    This controller uses dependency injection to communicate with the Application Layer through the IContactPersonService interface. It also maps incoming requests to DTOs, which are transformed to domain models inside the Application Layer.

    Responsibilities of API Layer

    • Handling HTTP requests and responses.

    • Applying validation and formatting errors.

    • Delegating business logic to the Application Layer.

    • Applying custom attributes for authorization and activity logging (e.g., AuthorizePermission and ActivityLog).

    4.2. Application Layer (Contact.Application)

    The Application Layer handles the core business logic and coordinates workflows. It acts as a middle layer between the API and Domain Layers. This layer uses AutoMapper to map DTOs to domain entities and back, keeping input/output data separate from the domain model.

    Example: ContactPersonService

    using AutoMapper;
    using Contact.Application.Interfaces;
    using Contact.Application.UseCases.ContactPerson;
    using Contact.Domain.Entities;
    using Contact.Domain.Interfaces;
    namespace Contact.Application.Services;
    public class ContactPersonService : GenericService<ContactPerson, ContactPersonResponse, CreateContactPerson, UpdateContactPerson>, IContactPersonService
    {
    public ContactPersonService(IGenericRepository<ContactPerson> repository, IMapper mapper, IUnitOfWork unitOfWork)
    : base(repository, mapper, unitOfWork)
    {
    }
    // Additional methods specific to ToDo can go here if needed
    }
    using AutoMapper;
    using Contact.Application.Interfaces;
    using Contact.Domain.Entities;
    using Contact.Domain.Interfaces;
    namespace Contact.Application.Services;
    public class GenericService<TEntity, TResponse, TCreate, TUpdate>
    : IGenericService<TEntity, TResponse, TCreate, TUpdate>
    where TEntity : BaseEntity
    where TResponse : class
    where TCreate : class
    where TUpdate : class
    {
    private readonly IGenericRepository<TEntity> _repository;
    private readonly IMapper _mapper;
    private readonly IUnitOfWork _unitOfWork;
    public GenericService(IGenericRepository<TEntity> repository, IMapper mapper, IUnitOfWork unitOfWork)
    {
    _repository = repository;
    _mapper = mapper;
    _unitOfWork = unitOfWork;
    }
    public async Task<TResponse> Add(TCreate createDto)
    {
    using var transaction = _unitOfWork.BeginTransaction();
    try
    {
    var entity = _mapper.Map<TEntity>(createDto);
    var createdEntity = await _repository.Add(entity, transaction);
    await _unitOfWork.CommitAsync();
    return _mapper.Map<TResponse>(createdEntity); // Map to clean response DTO
    }
    catch
    {
    await _unitOfWork.RollbackAsync();
    throw;
    }
    }
    public async Task<TResponse> FindByID(Guid id)
    {
    var entity = await _repository.FindByID(id);
    return _mapper.Map<TResponse>(entity); // Map to clean response DTO
    }
    public async Task<IEnumerable<TResponse>> FindAll()
    {
    var entities = await _repository.FindAll();
    return _mapper.Map<IEnumerable<TResponse>>(entities); // Map to clean response DTOs
    }
    }

    Responsibilities of the Application Layer

    • Implements business logic and rules.

    • Coordinates between controllers and repositories.

    • Contains the DTOs (Data Transfer Objects) for create, update, and response.

    • Uses AutoMapper to map DTOs to domain entities.

    • Runs validation through FluentValidation.

    4.2.1 AutoMapper for DTO and Entity Mapping

    AutoMapper handles the conversion of DTOs to domain entities and back.

    Example: AutoMapper Configuration

    public class ContactPersonMappingProfile : Profile
    {
    public ContactPersonMappingProfile()
    {
    CreateMap<CreateContactPersonDto, ContactPerson>();
    CreateMap<ContactPerson, ContactPersonResponse>();
    }
    }

    4.3. Domain Layer (Contact.Domain)

    The Domain Layer contains the core business logic and entities. It’s completely decoupled from external systems, and all business rules reside in this layer.

    Here’s an example of the ContactPerson entity:

    Example: ContactPerson Entity

    public class ContactPerson : BaseEntity
    {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public long Mobile { get; set; }
    public string Email { get; set; }
    }

    This ContactPerson entity extends the BaseEntity class, which provides common properties like Id, CreatedOn, and UpdatedOn. These entities represent the core business data and are used in business logic across the application.

    4.4. Infrastructure Layer (Contact.Infrastructure)

    The Infrastructure Layer implements external system integrations, like databases or logging services. This layer interacts with the Application Layer via interfaces, keeping the coupling loose.

    Example: ContactPersonRepository

    public class ContactPersonRepository : IContactPersonRepository
    {
    private readonly IDapperHelper _dapperHelper;
    public ContactPersonRepository(IDapperHelper dapperHelper)
    {
    _dapperHelper = dapperHelper;
    }
    public async Task<ContactPerson> AddAsync(ContactPerson contact)
    {
    var sql = @"INSERT INTO Contacts (FirstName, LastName, Mobile, Email, CreatedOn)
    VALUES (@FirstName, @LastName, @Mobile, @Email, @CreatedOn)";
    await _dapperHelper.ExecuteAsync(sql, contact);
    return contact;
    }
    }

    This repository uses Dapper rather than Entity Framework. That was a deliberate choice for this project: the SQL stays visible and the repository interfaces keep it swappable, which the Dapper article covers in detail.

    The full series

    This article is the starting point for a series built around the Contact Management Application. Each post takes one concern and shows the working implementation:

    1. Clean Architecture: Introduction to the Project Structure (You are here)

    2. Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging

    3. Clean Architecture: Validating Inputs with FluentValidation

    4. Clean Architecture: Dependency Injection Setup Across Layers

    5. Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)

    6. Clean Architecture: Implementing Activity Logging with Custom Attributes

    7. Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions

    8. Clean Architecture: Using Dapper for Data Access and Repository Pattern

    9. Clean Architecture: Best Practices for Creating and Using DTOs in the API

    10. Clean Architecture: Error Handling and Exception Management in the API

    11. Clean Architecture: Dockerizing the .NET Core API, Angular and MS SQL Server

    12. Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.