Building ASP.NET Core Apps with Clean Architecture

You are currently viewing Building ASP.NET Core Apps with Clean Architecture

In my previous post, A Guide for Building Software with Clean Architecture, I explained how clean architecture allows developers to build testable, maintainable, and flexible software systems. We learned the benefits, principles, and challenges of using clean architecture. We learned how clean architecture focuses on dividing the software into independent and loosely coupled layers in a way that code modifications done in one layer don’t affect the code in other layers. In this post, I will show you how to implement the clean architecture in an ASP.NET Core Web API project. You will learn how to organize the application code into separate layers or modules with a specific responsibility.

A Quick Introduction to Clean Architecture

In the context of ASP.NET Core, the clean architecture is implemented by organizing the application code into multiple layers which can contain one or more projects each with a specific responsibility. These layers typically include:

Domain Layer = This layer is the backbone of the clean architecture and all other projects defined in other layers should depend on this layer. This layer is highly abstracted and it contains domain entities, events, value objects, aggregates, etc.   

Application Layer = The application business rules and use cases are available in this layer. Mostly it defines interfaces that are implemented by the outer layers. This layer contains business services, DTOs, mappers, validators, etc.

Infrastructure Layer = This layer contains the implementation of the interfaces defined in the Application layer. This layer is responsible for implementing the technical aspects of the system, such as data access, logging, and security, etc. It can have multiple projects using different third party libraries or frameworks.

Presentation Layer = This layer is responsible for presenting some user interface and handling user interaction with the system. It includes the views, controllers, and other web components.

Clean Architecture

Setting Up Clean Architecture Project in ASP.NET Core

Before we dive into the actual implementation details of the application, we need to decide what type of application we are about to build. I have to admit that I am fed up with typical e-commerce or shopping cart applications so I have decided to build something more interesting such as a fictitious football league application that can have entities such as football clubs, players, stadiums, countries, etc.

I have decided to use Visual Studio 2022 with .NET 7.0 to build the demo application so let’s get started.

  • Open Visual Studio 2022 and create a new blank solution with the name “CleanArchitectureDemo”.
  • Next, create the following four solution folders inside the blank solution.
    • Core
    • Infrastructure
    • Presentation
    • Shared
  • Next, create the following two class library projects in the Core solution folder.
    • CleanArchitectureDemo.Domain
    • CleanArchitectureDemo.Application
  • Next, create the following two class library projects in the Infrastructure folder
    • CleanArchitectureDemo.Infrastructure
    • CleanArchitectureDemo.Persistence
  • Next, create the following class library project in the Shared folder
    • CleanArchitectureDemo.Shared
  • Finally, create the following ASP.NET 7.0 Web API project in the Presentation folder
    • CleanArchitectureDemo.WebAPI

Once the above projects are created, the project structure in Visual Studio solution explorer will look similar to the following screenshot. 

ASP.NET Core Clean Architecture Project Structure

Before we start actual implementation, we need to add the following nuget packages in different projects of the solution.

CleanArchitectureDemo.Domain project needs the following nuget packages

CleanArchitectureDemo.Application project needs the following nuget packages

CleanArchitectureDemo.Persistence project needs the following nuget packages

CleanArchitectureDemo.WebAPI project needs the following nuget packages

I have already written multiple blog posts on some of the Nuget packages and libraries mentioned above and you can read the following posts to learn more about these packages.

  1. Mediator Design Pattern in ASP.NET Core
  2. Implement CQRS Pattern in ASP.NET Core 5
  3. A Step by Step Guide of using AutoMapper in ASP.NET Core
  4. ASP.NET Core Data Validations with FluentValidation
  5. Data Access in ASP.NET Core using EF Core (Database First)
  6. Data Access in ASP.NET Core using EF Core (Code First)

Implementing Clean Architecture Domain Layer

The domain layer is the core component of clean architecture and it represents the domain and use-case-independent business logic of the system. This layer has no dependency on any technology, third-party library, or frameworks. It includes interfaces, entities, and value objects. All other projects should depend on the Domain layer. For our demo project, let’s create the following two folders Common and Entities in the project.

ASP.NET Core Clean Architecture Domain Project

Common Folder

The Common folder contains base classes and interfaces.

IEntity.cs

This file contains a base IEntity interface and all domain entities will implement this interface either directly or indirectly.

public interface IEntity
{
    public int Id { get; set; }
}

IAuditableEntity.cs

This file defines a child interface of IEntity interfaces defined above. This interface adds additional properties to keep track of the entity’s audit trail information.

public interface IAuditableEntity : IEntity
{
    int? CreatedBy { get; set; }
    DateTime? CreatedDate { get; set; }
    int? UpdatedBy { get; set; }
    DateTime? UpdatedDate { get; set; }
}

IDomainEventDispatcher.cs

This interface declares a method that can be used to dispatch domain events throughout the application.

public interface IDomainEventDispatcher
{
    Task DispatchAndClearEvents(IEnumerable<BaseEntity> entitiesWithEvents);
}

BaseEvent.cs

This file contains the BaseEvent class that will become the base class of all domain events throughout the application. It only has one property DateOccurred that tells us when a particular event has occurred.

public abstract class BaseEvent : INotification
{
    public DateTime DateOccurred { get; protected set; } = DateTime.UtcNow;
}

BaseEntity.cs

This file contains BaseEntity class that implements the IEntity interface. The BaseEntity class also contains a collection of domain events and also some helper methods to add, remove, and clean domain events from this collection. The idea of storing events in the collection and then dispatching them once the entity is saved is first introduced by Jimmy Bogard in his blog post A better domain events pattern and it is used in many clean architectures implementations since then.

public abstract class BaseEntity : IEntity
{
    private readonly List<BaseEvent> _domainEvents = new();

    public int Id { get; set; }

    [NotMapped]
    public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();

    public void AddDomainEvent(BaseEvent domainEvent) => _domainEvents.Add(domainEvent);
    public void RemoveDomainEvent(BaseEvent domainEvent) => _domainEvents.Remove(domainEvent);
    public void ClearDomainEvents() => _domainEvents.Clear(); 
}

BaseAuditableEntity.cs

This class is the child class of BaseEntity and it implements the IAuditableEntity interface defined above.

public abstract class BaseAuditableEntity : BaseEntity, IAuditableEntity
{
    public int? CreatedBy { get; set; }
    public DateTime? CreatedDate { get; set; }
    public int? UpdatedBy { get; set; }
    public DateTime? UpdatedDate { get; set; }
}

Entities Folder

The Entities folder contains domain entities. All domain entities inherit from the above BaseAuditableEntity class.

READ ALSO:  Adapter Design Pattern in ASP.NET Core

Player.cs

public class Player : BaseAuditableEntity
{
    public string Name { get; set; }
    public int? ShirtNo { get; set; }
    public int? ClubId { get; set; }
    public string PhotoUrl { get; set; }
    public DateTime? BirthDate { get; set; }
}

The following diagram shows the relationships between different classes and interfaces defined in the Domain layer.

Clean Architecture Domain Layer

Implementing Clean Architecture Application Layer

The Application layer contains the business logic and defines abstractions in the form of interfaces. These interfaces are then implemented by outer layers. The application layer depends on the Domain layer and acts as a bridge between the Domain layer and external layers such as Persistence or Presentations layer. This layer contains business services, DTOs, Commands, Queries, etc.

ASP.NET Core Clean Architecture Application Project

Repositories Folder

This folder contains interfaces such as IUnitOfWork, IGenericRepository, and other domain-specific interfaces such as IPlayerRepository, etc. These interfaces define methods to read and update data.

IGenericRepository.cs

This interface defines a generic repository and it contains generic CRUD methods.

public interface IGenericRepository<T> where T : class, IEntity
{
    IQueryable<T> Entities { get; }

    Task<T> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

IUnitOfWork.cs

This interface defines a unit of work pattern that allows us to save changes made by multiple repositories at once.

public interface IUnitOfWork : IDisposable
{
    IGenericRepository<T> Repository<T>() where T : BaseAuditableEntity;
    Task<int> Save(CancellationToken cancellationToken);
    Task<int> SaveAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys);
    Task Rollback();
}

IPlayerRepository.cs

Most of the time, repositories will only use the generic methods defined in the IGenericRepository class but if they have some additional functionality and require a custom method implementation then these methods can be defined in specific repository interfaces. In the following example, IPlayerRepository is defining an additional method to get players by the club.

public interface IPlayerRepository
{
    Task<List<Player>> GetPlayersByClubAsync(int clubId);
}

Features Folder

In the MVC design pattern, the code is organized by types which means you normally have a Controllers folder that contains a huge list of controllers and a Models folder that contains a huge list of models. If you are working on a particular feature, then it is difficult to find related controllers, models, etc. In clean architecture, it is a standard convention to organize the application code by functional features. This makes it easier to find all the related classes in one place and to understand and maintain the codebase over time.

If you want to have multiple features related to a particular entity or domain then you can create a separate subfolder for each feature e.g. Players, Clubs, Stadiums, etc. Each feature folder can contain subfolders such as Queries and Commands which can contain CQRS commands and queries.

ASP.NET Core Clean Architecture Application Features Folders

Check how features are organized in two subfolders Commands and Queries which are using CQRS pattern to separate reads and writes. If you are not familiar with the CQRS pattern then read my post Implement CQRS Pattern in ASP.NET Core 5

The Commands folder inside the Players folder has features related sub-folders such as:

  1. CreatePlayer – To implement the functionality of creating new players in the database.
  2. UpdatePlayer – To implement the functionality of updating players in the database.
  3. DeletePlayer – To implement the functionality of deleting players from the database.

The Queries folder inside Players has features related sub-folders such as:

  1. GetAllPlayers – To implement the functionality of fetching all players from the database.
  2. GetPlayerById – To implement the functionality of fetching a single player by matching the id from the database.
  3. GetPlayersByClub – To implement the functionality of fetching all players by matching clubs from the database.
  4. GetPlayersWithPagination – To implement the functionality of fetching all players with pagination from the database.

Each of the above feature folders can contain Commands, Queries, Handlers, Events, DTOs, Validators, and other components specific to that particular feature. Let’s review the files and the code available in the CreatePlayer feature.

CreatePlayerCommand.cs

public record CreatePlayerCommand : IRequest<Result<int>>, IMapFrom<Player>
{
    public string Name { get; set; }
    public int ShirtNo { get; set; }
    public string PhotoUrl { get; set; }
    public DateTime BirthDate { get; set; }
}

internal class CreatePlayerCommandHandler : IRequestHandler<CreatePlayerCommand, Result<int>>
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;

    public CreatePlayerCommandHandler(IUnitOfWork unitOfWork, IMapper mapper)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper; 
    }

    public async Task<Result<int>> Handle(CreatePlayerCommand command, CancellationToken cancellationToken)
    {
        var player = new Player()
        {
            Name = command.Name,
            ShirtNo = command.ShirtNo,
            PhotoUrl = command.PhotoUrl,
            BirthDate = command.BirthDate
        };

        await _unitOfWork.Repository<Player>().AddAsync(player);
        player.AddDomainEvent(new PlayerCreatedEvent(player));
        await _unitOfWork.Save(cancellationToken);
        return await Result<int>.SuccessAsync(player.Id, "Player Created.");
    }
}

This file contains one record CreatePlayerCommand and CreatePlayerCommandHandler. You can separate these two into two separate files but traditionally most developers prefer to keep these two together in the same file because it is easier to see both the command and the handler in one place and quickly understand what is going on rather than jumping from one file to another file.

The CreatePlayerCommand is defined as a C# record and not as a class because records only represent a set of data without any functionality. The CreatePlayerCommandHandler class implements IRequestHandler<TRequest, TResponse> interface available in the MediatR library. We are passing CreatePlayerCommand as TRequest and Result<int> object as TResponse. The Result class is our class which is defined in a CleanArchitectureDemo.Shared library.

READ ALSO:  12 Common Mistakes in Implementing Clean Architecture

The actual functionality is implemented inside the Handle method of CreatePlayerCommandHandler. First, we created a new player entity and mapped the properties of the command object with the player entity.

var player = new Player()
{
    Name = command.Name,
    ShirtNo = command.ShirtNo,
    PhotoUrl = command.PhotoUrl,
    BirthDate = command.BirthDate
};

Next, we are adding the player in the database using the generic AddAsync method.

await _unitOfWork.Repository<Player>().AddAsync(player);

Next, we are adding the PlayerCreatedEvent event in the domain events collection available in BaseEntity class.

player.AddDomainEvent(new PlayerCreatedEvent(player));

Next, we are

await _unitOfWork.Save(cancellationToken);

Finally, we are sending the Result with the newly created player Id and the message “Player Created”.

return await Result<int>.SuccessAsync(player.Id, "Player Created.");

PlayerCreatedEvent.cs

The PlayerCreatedEvent domain event inherits from the BaseEvent class we created earlier in the CleanArchitectureDemo.Domain project. This event will carry the newly created player object so that the event handler/subscriber can read the information from the player object.

public class PlayerCreatedEvent : BaseEvent
{
    public Player Player { get; }

    public PlayerCreatedEvent(Player player)
    {
        Player = player;
    }
}

Running the CreatePlayerCommand through an ASP.NET Web API will look something like the following screenshot.

ASP.NET Core Clean Architecture CQRS Create Command

GetPlayersWithPagination.cs

This file contains GetPlayersWithPaginationQuery and GetPlayersWithPaginationQueryHandler. We are passing GetPlayersWithPaginationQuery as TRequest and PaginatedResult<GetPlayersWithPaginationDto> object as TResponse. The PaginatedResult class is our custom class and it is defined in a CleanArchitectureDemo.Shared library.

public record GetPlayersWithPaginationQuery : IRequest<PaginatedResult<GetPlayersWithPaginationDto>>
{
    public int PageNumber { get; set; }
    public int PageSize { get; set; }

    public GetPlayersWithPaginationQuery() { }

    public GetPlayersWithPaginationQuery(int pageNumber, int pageSize)
    {
        PageNumber = pageNumber;
        PageSize = pageSize;
    } 
}

internal class GetPlayersWithPaginationQueryHandler : IRequestHandler<GetPlayersWithPaginationQuery, PaginatedResult<GetPlayersWithPaginationDto>>
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;

    public GetPlayersWithPaginationQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper;
    }

    public async Task<PaginatedResult<GetPlayersWithPaginationDto>> Handle(GetPlayersWithPaginationQuery query, CancellationToken cancellationToken)
    {
        return await _unitOfWork.Repository<Player>().Entities
               .OrderBy(x => x.Name) 
               .ProjectTo<GetPlayersWithPaginationDto>(_mapper.ConfigurationProvider)
               .ToPaginatedListAsync(query.PageNumber, query.PageSize, cancellationToken);
    }
}

Inside the Handle method, we are using the ProjectTo<T> extension method available in AutoMapper.QueryableExtensions namespace. This method maps IQueryable<Player> to IQueryable< GetPlayersWithPaginationDto> using the AutoMapper mapping engine. The ToPaginatedListAsync is our custom extension method that creates a paginated result.

public static async Task<PaginatedResult<T>> ToPaginatedListAsync<T>(this IQueryable<T> source, int pageNumber, int pageSize, CancellationToken cancellationToken) where T : class
{
    pageNumber = pageNumber == 0 ? 1 : pageNumber;
    pageSize = pageSize == 0 ? 10 : pageSize;
    int count = await source.CountAsync();
    pageNumber = pageNumber <= 0 ? 1 : pageNumber;
    List<T> items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
    return PaginatedResult<T>.Create(items, count, pageNumber, pageSize);
}

GetPlayersWithPaginationDto.cs

The above query and handler are using the following DTO class also defined in the GetPlayersWithPagination folder.

public class GetPlayersWithPaginationDto : IMapFrom<Player>
{
    public int Id { get; init; }
    public string Name { get; init; }
    public int ShirtNo { get; init; }
    public int HeightInCm { get; init; }
    public string FacebookUrl { get; init; }
    public string TwitterUrl { get; init; }
    public string InstagramUrl { get; init; }
    public int DisplayOrder { get; init; }
}

Running the GetPlayersWithPaginationQuery through an ASP.NET Web API will look something like the following screenshot.

ASP.NET Core Clean Architecture CQRS Get Paginated Results

GetPlayersWithPaginationValidator.cs

The GetPlayersWithPagination folder also has the following GetPlayersWithPaginationValidator class that can be used to validate the input parameters of the query.

public class GetPlayersWithPaginationValidator : AbstractValidator<GetPlayersWithPaginationQuery>
{
    public GetPlayersWithPaginationValidator()
    {
        RuleFor(x => x.PageNumber)
            .GreaterThanOrEqualTo(1)
            .WithMessage("PageNumber at least greater than or equal to 1.");

        RuleFor(x => x.PageSize)
            .GreaterThanOrEqualTo(1)
            .WithMessage("PageSize at least greater than or equal to 1.");
    }
}

Interfaces Folder

All interfaces related to application and business services can be defined in this folder. Here is one example of such a service.

IEmailService.cs

public interface IEmailService
{
    Task SendAsync(EmailRequestDto request);
}

IServiceCollectionExtensions.cs (CleanArchitectureDemo.Application)

To configure the dependencies defined in the Application layer, the following class is added to the project. 

public static class IServiceCollectionExtensions
{
    public static void AddApplicationLayer(this IServiceCollection services)
    {
        services.AddAutoMapper();
        services.AddMediator();
        services.AddValidators();
    }

    private static void AddAutoMapper(this IServiceCollection services)
    {
        services.AddAutoMapper(Assembly.GetExecutingAssembly());
    }

    private static void AddMediator(this IServiceCollection services)
    {
        services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
    }

    private static void AddValidators(this IServiceCollection services)
    {
        services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
    }        
}

Implementing Clean Architecture Infrastructure Layer

This layer contains the implementation of the interfaces defined in the Application layer. The project(s) defined in this layer communicate with external systems and technologies, such as databases, APIs, or cloud services. This layer should only interact with the domain layer through the application layer and should not contain any business logic or domain knowledge. The main goal of the infrastructure layer is to encapsulate the technical details of the application so that they can be easily changed or replaced without affecting the rest of the application.

It is also very common practice to create multiple infrastructure projects in this layer especially if you are working on a very large project where you need to communicate with multiple data sources or external systems. In our demo app, we created two infrastructure projects CleanArchitectureDemo.Infrastructure and CleanArchitectureDemo.Persistence.

ASP.NET Core Clean Architecture Infrastructure Projects

The CleanArchitectureDemo.Infrastructure project will implement the business services defined in the application layer such as the following.

EmailService.cs

public class EmailService : IEmailService
{ 
    public async Task SendAsync(EmailRequestDto request)
    {
        var emailClient = new SmtpClient("localhost");
        var message = new MailMessage
        {
            From = new MailAddress(request.From),
            Subject = request.Subject,
            Body = request.Body
        };
        message.To.Add(new MailAddress(request.To));
        await emailClient.SendMailAsync(message);
    }
}

IServiceCollectionExtensions.cs (CleanArchitectureDemo.Infrastructre)

All business services defined in the application layer are registered with the dependency injection container.

public static class IServiceCollectionExtensions
{
    public static void AddInfrastructureLayer(this IServiceCollection services)
    {
        services.AddServices();
    }

    private static void AddServices(this IServiceCollection services)
    {
        services
            .AddTransient<IEmailService, EmailService>();
    }
}

The CleanArchitectureDemo.Persistence project contains data contexts, repositories, migrations, etc.

Contexts Folder

There can be multiple database contexts in a large project so it is always a good idea to create a separate folder for all context classes. Currently, this folder will only contain ApplicationDbContext.cs file.

ApplicationDbContext.cs

This file contains the EntityFramework DbContext. The important code is available in the SaveChangesAsync method where we are checking if the entity we are saving has some domain events associated with it and if there are some events available we are dispatching them all using our IDomainEventDispatcher.

public class ApplicationDbContext : DbContext
{
    private readonly IDomainEventDispatcher _dispatcher;

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
      IDomainEventDispatcher dispatcher)
        : base(options)
    {
        _dispatcher = dispatcher;
    }

    public DbSet<Club> Clubs => Set<Club>();
    public DbSet<Player> Players => Set<Player>();
    public DbSet<Stadium> Stadiums => Set<Stadium>();
    public DbSet<Country> Countries => Set<Country>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        int result = await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        
        if (_dispatcher == null) return result;

        var entitiesWithEvents = ChangeTracker.Entries<BaseEntity>()
            .Select(e => e.Entity)
            .Where(e => e.DomainEvents.Any())
            .ToArray();

        await _dispatcher.DispatchAndClearEvents(entitiesWithEvents);

        return result;
    }

    public override int SaveChanges()
    {
        return SaveChangesAsync().GetAwaiter().GetResult();
    }
}

Repositories Folder

The Repositories folder contains the implementation of the repository and unit of work interfaces.

READ ALSO:  A Step by Step Guide of using AutoMapper in ASP.NET Core

GenericRepository.cs

The implementation of GenericRepository is very straightforward. It uses ApplicationDbContext to perform standard CRUD operations.

public class GenericRepository<T> : IGenericRepository<T> where T : BaseAuditableEntity
{
    private readonly ApplicationDbContext _dbContext;

    public GenericRepository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IQueryable<T> Entities => _dbContext.Set<T>();

    public async Task<T> AddAsync(T entity)
    {
        await _dbContext.Set<T>().AddAsync(entity);
        return entity;
    }

    public Task UpdateAsync(T entity)
    {
        T exist = _dbContext.Set<T>().Find(entity.Id);
        _dbContext.Entry(exist).CurrentValues.SetValues(entity);
        return Task.CompletedTask;
    }

    public Task DeleteAsync(T entity)
    {
        _dbContext.Set<T>().Remove(entity);
        return Task.CompletedTask;
    }

    public async Task<List<T>> GetAllAsync()
    {
        return await _dbContext
            .Set<T>()
            .ToListAsync();
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _dbContext.Set<T>().FindAsync(id);
    }
}

PlayerRepository.cs

If any repository has any custom method, it can be defined in its respective repository. In our demo app, the IPlayerRepository has a custom method GetPlayersByClubAsync which is implemented in PlayerRespository below.

public class PlayerRepository : IPlayerRepository
{
    private readonly IGenericRepository<Player> _repository;

    public PlayerRepository(IGenericRepository<Player> repository) 
    {
        _repository = repository;
    }

    public async Task<List<Player>> GetPlayersByClubAsync(int clubId)
    {
        return await _repository.Entities.Where(x => x.ClubId == clubId).ToListAsync();
    }
}

UnitOfWork.cs

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _dbContext;
    private Hashtable _repositories;
    private bool disposed;

    public UnitOfWork(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    public IGenericRepository<T> Repository<T>() where T : BaseAuditableEntity
    {
        if (_repositories == null)
            _repositories = new Hashtable();

        var type = typeof(T).Name;

        if (!_repositories.ContainsKey(type))
        {
            var repositoryType = typeof(GenericRepository<>);

            var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), _dbContext);

            _repositories.Add(type, repositoryInstance);
        }

        return (IGenericRepository<T>) _repositories[type];
    }

    public Task Rollback()
    {
        _dbContext.ChangeTracker.Entries().ToList().ForEach(x => x.Reload());
        return Task.CompletedTask;
    }

    public async Task<int> Save(CancellationToken cancellationToken)
    {
        return await _dbContext.SaveChangesAsync(cancellationToken);
    } 
}

IServiceCollectionExtensions.cs (CleanArchitectureDemo.Persistence)

To configure the dependencies defined in the Persistence project, the following class is added in the project. 

public static class IServiceCollectionExtensions
{
    public static void AddPersistenceLayer(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext(configuration);
        services.AddRepositories();
    }

    public static void AddDbContext(this IServiceCollection services, IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection");

        services.AddDbContext<ApplicationDbContext>(options =>
           options.UseSqlServer(connectionString,
               builder => builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
    }

    private static void AddRepositories(this IServiceCollection services)
    {
        services
            .AddTransient(typeof(IUnitOfWork), typeof(UnitOfWork))
            .AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>))
            .AddTransient<IPlayerRepository, PlayerRepository>()
            .AddTransient<IClubRepository, ClubRepository>()
            .AddTransient<IStadiumRepository, StadiumRepository>()
            .AddTransient<ICountryRepository, CountryRepository>();
    }
}

Implementing Clean Architecture Presentation Layer

The presentation layer is the out most layer of the clean architecture and it presents some GUI or exposes some public API to interact directly with the end user or other client applications. The presentation layer should not contain business logic or domain knowledge and should only interact with the rest of the application through the application layer. The presentation layer can be implemented using various technologies, such as a Web API, gRPC, or MVC Web Application, etc. For our demo application, we created an ASP.NET Core 7.0 Web API project that will expose some RESTful endpoints to perform CRUD operations on Player entity.

Controllers Folder

In a Web API project, we usually create API controllers in the Controllers folder. These controllers handle incoming HTTP requests and return the appropriate HTTP response. For our demo application, let’s create a PlayersController class.

PlayersControllers.cs

public class PlayersController : ApiControllerBase
{
    private readonly IMediator _mediator;

    public PlayersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<ActionResult<Result<List<GetAllPlayersDto>>>> Get()
    {
        return await _mediator.Send(new GetAllPlayersQuery());
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Result<GetPlayerByIdDto>>> GetPlayersById(int id)
    {
        return await _mediator.Send(new GetPlayerByIdQuery(id)); 
    }

    [HttpGet]
    [Route("club/{clubId}")]
    public async Task<ActionResult<Result<List<GetPlayersByClubDto>>>> GetPlayersByClub(int clubId)
    {
        return await _mediator.Send(new GetPlayersByClubQuery(clubId));
    }

    [HttpGet]
    [Route("paged")]
    public async Task<ActionResult<PaginatedResult<GetPlayersWithPaginationDto>>> GetPlayersWithPagination([FromQuery] GetPlayersWithPaginationQuery query)
    {
        var validator = new GetPlayersWithPaginationValidator();
        var result = validator.Validate(query);

        if (result.IsValid)
        {
            return await _mediator.Send(query);
        }

        var errorMessages = result.Errors.Select(x => x.ErrorMessage).ToList();
        return BadRequest(errorMessages); 
    }

    [HttpPost]
    public async Task<ActionResult<Result<int>>> Create(CreatePlayerCommand command)
    {
        return await _mediator.Send(command);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult<Result<int>>> Update(int id, UpdatePlayerCommand command)
    {
        if (id != command.Id)
        {
            return BadRequest();
        }

        return await _mediator.Send(command); 
    }

    [HttpDelete("{id}")]
    public async Task<ActionResult<Result<int>>> Delete(int id)
    {
        return await _mediator.Send(new DeletePlayerCommand(id)); 
    }
}

Program.cs

The Program.cs file is the entry point of the application. The Program.cs configure the services required by the application, and also defines the application request handling pipeline. In our demo app, we will call the extension methods AddApplicationLayer, AddInfrastructureLayer, and AddPersistenceLayer defined in different layers.

using CleanArchitectureDemo.Application.Extensions;
using CleanArchitectureDemo.Infrastructure.Extensions;
using CleanArchitectureDemo.Persistence.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddApplicationLayer();
builder.Services.AddInfrastructureLayer();
builder.Services.AddPersistenceLayer(builder.Configuration);

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage(); 
}

app.MapControllers();

app.Run();

FAQs

What is Clean Architecture?

Clean Architecture is a software design pattern that emphasizes the separation of concerns and the separation of dependencies in order to achieve a highly maintainable and testable codebase. It promotes a clear and modular structure, making it easier to understand and modify the application over time.

What are the benefits of using Clean Architecture?

Some key benefits of Clean Architecture in ASP.NET Core include improved maintainability, testability, flexibility, and scalability. It allows for better separation of concerns, making it easier to understand and modify different parts of the application independently.

How does Clean Architecture promote testability?

Clean Architecture encourages the use of interfaces and dependency injection, which makes it easier to write unit tests for individual components or modules of the application. By isolating dependencies and relying on abstractions, you can mock or substitute dependencies during testing, resulting in more effective and isolated unit tests.

What are the main components of Clean Architecture?

Clean Architecture typically consists of several layers, including the Domain Layer (representing the core business logic), Application Layer (containing application-specific use cases and services), Infrastructure Layer (dealing with data access and external dependencies), and Presentation Layer (handling user interfaces and interaction).

How does ASP.NET Core integrate with Clean Architecture?

ASP.NET Core can be easily integrated with Clean Architecture principles. The framework provides the necessary tools and features to implement a clean and modular architecture, such as dependency injection, middleware pipeline, and the MVC pattern. This allows developers to build scalable, maintainable, and testable applications using Clean Architecture principles.

Is Clean Architecture suitable for all ASP.NET Core projects?

Clean Architecture is particularly useful for larger or complex projects where maintainability and flexibility are critical. However, for smaller or straightforward projects, the overhead of implementing Clean Architecture may not be necessary or practical.

Conclusion

In the ASP.NET Core context, the clean architecture is usually implemented by organizing the application code into separate layers or modules in such a way that each has a specific responsibility. In this article, I tried to give you a detailed example of implementing clean architecture in ASP.NET Core. The complete source code of our CleanArchitectureDemo application is available on GitHub. If you have any comments or suggestions, please leave your comments below. Don’t forget to share this tutorial with your friends or community.

This Post Has 25 Comments

  1. R

    Hi my friend. Congratulation for your tutorial. Where could we find the script to create the DB, before to launch the api service?
    Or we need to do by ourself?
    Thanks! 🙂

  2. aanand kumar

    i need a Login Tutorial link in clean archicture with jwt token authorisation can anyone help me .

  3. Ronaldo

    Why would an API have reference to Persistence?

  4. Eddie A

    How would you implement Identity (without IdentityServer) in this solution or do you have a blog that already covers this? Thanks! 🙂

  5. Burak

    Thank you

  6. Dwight DePass

    Might you have the Database.sql script. So I can recreate the database.

  7. JayKit

    Hello, I cannot use dotnet ef migrations. The response is: unable to create an object of type context. for the different patterns supported at design time

    1. Waqas Anwar

      You don’t need to use migrations with my reference project as I am using EF Core with database first approach and connecting to an existing SQL Server database.

    2. Ronaldo

      Using this project structure you won’t be able to do ef migrations. His Database Context is in Infrastructure layer and have reference to Domain Entities.

  8. Rahul

    Hi Anwar, We dont want to use mediatr….. can we still implement this architecture?

    1. Waqas Anwar

      Yes, you can implement your own application services and call repositories in your services.

  9. Burt Gardner

    coming from a database background, can the database architecture diagrams be provided ?

  10. Zeeshan

    What is the purpose of AddDomainEvent method and why are we adding our events in AddDomainEvent method.

    1. Waqas Anwar

      Instead of dispatching to a domain event immediately, It is a much better approach to add/record all events before committing the transaction and dispatch all events once the transaction is committed. This approach has several benefits such as they make the testing of events much simpler because the global domain event dispatcher is not firing events all the time.

  11. Prasanth Balakrishnan

    You have not specified the implementation of various class such as Result, PaginatedResult etc in the above article.

    1. Waqas Anwar

      I excluded them from blog post to keep the post shorter but you can find them in the source code of the demo project I uploaded on Github.

      1. Bryan Makini

        In the source code you also did not implement them too

  12. Florent

    You say that the domain layer has no external dependencies to third party libraries. But you need to install MediatR into the CleanArchitectureDemo.Domain project, Why is that?

    1. Waqas Anwar

      I added Mediatr package because I want to publish domain events using Mediatr from a central place like I did inside DomainEventDispatcher class. I mentioned in the post that this type of domain events pattern is recommended by Jimmy Bogard in one of his post and I liked this approach. Obviously, if you are using some other approach of handling domain events then you don’t need to add Mediatr package reference.

  13. Daniel

    Why is EF.Core is referenced in the Application layer?

    1. Waqas Anwar

      We need EF Core because we need to use EF Core methods in CQRS Commands and Queries.

  14. Ronaldinho

    Amazing! Everything is explained in detail with such simplicity. I would suggest if you have time to add Blazor as follow up tutorial to complete the frontend application and tutorial set. Thumbs up!

  15. Evgenii Shmanev

    Let me put my 5 cents. In spite, the overall solution is quite good, but it has some drawbacks. I would be happy if you’ll find my comment useful.

    – Creating a common interface for entities with identifier makes impossible to use composite keys referencing other entities. For instance, A possible composite key might be a pair of two referenced entities. That’s why I prefer to not define the Id property in the base inteface/class.

    – There are no strict aggregate roots (aka IAggregateRoot). It might cause some spaghetti code that saves entities. It would be better to strictly distinguish aggregate roots and dependents that cannot exist without a root.

    – Domain event dispatcher represents an infrastructure layer. Entities usually cannot subscribe on domain events, because a domain event usually represents a side effect of a transaction.

    – In spite the fact that implementation of a generic repository is quite common scenario, it rarely works in complex cases, e.g. if a result of a repo should be an aggregated object of several domain entities or an entity has a composite key. In such cases, I personally prefer query objects. (BTW, if I use Nhibernate, I tend to decline re-defining repos/UoW, because ISession/ITransaction represent the same).

    – When you invoke UnitOfWork.Save() directly in the handler, the Mediator pipeline might become broken or cause inconsistent data, because other pipes also might be interested in data modification. I would rather move transaction commit to a custom Mediator pipe that commits all changes on finish.

    – It would be better to return HTTP statuses in controllers alongside results. For example, it can be done on this way:

    [ProducesResponseType(typeof(CreatePlayerResponse)), StatusCodes.Http201Created]
    public async Task Create(CreatePlayerCommand command)
    {
    var result = await mediator.Send(command);
    var playerUrl = Url.Action(nameof(GetPlayerById), new { result.PlayerId });
    return Created(playerUrl, result);
    }

    1. Waqas Anwar

      I read all your points and I really appreciate you spend some time to give this valuable feedback. I agree with you that these points can improve my clean architecture implementation even further.

Leave a Reply