Clean Architecture in ASP.NET Core 6 with CQRS

In this article, we will implement the clean architecture before starting the implementation.

Let’s have a little discussion about what is clean architecture and what we mean by CQRS.

Clean architecture is based on the foundation of the SOLID principle, which provides responsibility segregation and re-usability.

There are three layers in Clean architecture.

  • Domain
  • Application
  • Infrastructure

Domain is considered the core, which usually consists of entities and interfaces that could be shared among different systems. It does not have implementation and does not depend on a database.

Application Layer Consists of the business logic of the application.

The infrastructure layer consists of DAL implementation, which means we will have EF if we implement it,

Web services, all communication outside of the application, Email, SMS, etc. will be in this layer.

Any other services which are based are not based on business logic will be here.

Let’s start the implementation; we are taking the example of the Food ordering application.

Create a Web API project.

Web API 

Create three class library

Class

With the following structure.

Structure

Core Layer

The core will have database Entities and Repository Interface.

Let's create the Interface for the base Repository.

it will be of generic type and will have all the basic functions we need to perform on entities 

Now Create the Order Entity and Order Status.

public class Order
{
    [Required]
    public Guid OrderID { get; set; }

    [Required]
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long ID { get; set; }

    public long UserID { get; set; }
    public DateTime CreatedDate { get; set; }
    public string OrderDescription { get; set; }
    public virtual OrderStatus Status { get; set; }
    public bool isActive { get; set; }
    public bool isDeleted { get; set; }
}
public class OrderStatus
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string Value { get; set; }
    public bool isActive { get; set; }
    public bool isDeleted { get; set; }
}

Now that we have created the Base repository with all base functions for any further function for specific entities, we will add that entity repository.

public interface IOrderRepository
{
    Task<IEnumerable<Order>> GetOrderByStatus(string type);
}

This is how the core will look now.

Core

Infrastructure Layer

Infrastructure will have DAL implementation or another service that will be interacting with other systems.

Let’s create a Database Context that will have a database implementation.

public class FOAContext : DbContext
{
    public FOAContext(DbContextOptions<FOAContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .HasIndex(p => new { p.UserID })
            .IsUnique(true);

        modelBuilder.Entity<Order>()
            .HasIndex(p => new { p.OrderID })
            .IsUnique(true);
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }
}

Now Implement the Base repository.

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly FOAContext _userContext;

    public Repository(FOAContext userContext)
    {
        _userContext = userContext;
    }

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

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

    public async Task DeleteAsync(T entity)
    {
        _userContext.Set<T>().Remove(entity);
        await _userContext.SaveChangesAsync();
    }

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

    public async Task UpdateAsync(T entity)
    {
        _userContext.Set<T>().Update(entity);
        await _userContext.SaveChangesAsync();
    }
}

Now we need to implement the Order repository that will inherit the Base Repository.

public class OrderRepository : Repository<Order>, IOrderRepository
{
    public OrderRepository(FOAContext userContext) : base(userContext)
    {
    }

    public async Task<IEnumerable<Order>> GetOrderByStatus(string type)
    {
        return await _userContext.Set<Order>().Where(x => x.Status.Value == type).ToListAsync();
    }
}

This is how the Infrastructure will look like this.

Infrastructure

Application Layer

Now we will implement the Application layer with all the business logic and CQRS implementation.

We will have mappers and other services that will be used in the application layer

Features

In the Order feature, we will have commands and queries.

All the Update, Add, and Delete functions will be in commands.

All the Read Queries will be under Queries.

Order

To implement the CQRS, we will have to create CreateOrderRequest, CreateOrderResponse, and CreateOrderHandler.

The handler will receive the request and perform business logic will return a response.

Now create a request and Response class for our Create command.

public Guid OrderID { get; set; }
public long UserID { get; set; }
public DateTime CreatedDate { get; set; }
public string OrderDescription { get; set; }
public int OrderStatusID { get; set; }
public virtual OrderStatus Status { get; set; }
public virtual User User { get; set; }
public bool isActive { get; set; }
public bool isDeleted { get; set; }

Now we will create a Mapping profile.

public class OrderMapperProfile : Profile
{
    public OrderMapperProfile()
    {
        CreateMap<Order, GetAllOrderResponse>()
            .ForMember(dest =>
                dest.OrderStatus,
                opt => opt.MapFrom(src => Enums.GetOrderStatusValue(src.OrderStatusID)));

        CreateMap<CreateOrderRequest, Order>();
    }
}
public class FOAMapping
{
    private static readonly Lazy<IMapper> Lazy = new Lazy<IMapper>(() =>
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.GetMethod.IsAssembly;
            cfg.AddProfile<UserMapperProfile>();
            cfg.AddProfile<OrderMapperProfile>();
        });
        var mapper = config.CreateMapper();
        return mapper;
    });

    public static IMapper Mapper => Lazy.Value;
}

Now we need to create the handler that will have the business logic.

public class CreateOrderHandler : IRequestHandler<CreateOrderRequest, CreateOrderResponse>
{
    private readonly IOrderRepository _orderRepository;

    public CreateOrderHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task<CreateOrderResponse> Handle(CreateOrderRequest request, CancellationToken cancellationToken)
    {
        var orderEntity = FOAMapping.Mapper.Map<Order>(request);
        if (orderEntity is null)
        {
            throw new ApplicationException("Issue with mapper");
        }

        orderEntity.OrderID = Guid.NewGuid();
        orderEntity.OrderStatusID = Enums.GetOrderStatusValue("InProcess");

        var newOrder = await _orderRepository.AddAsync(orderEntity);
        var orderResponse = FOAMapping.Mapper.Map<CreateOrderResponse>(newOrder);
        return orderResponse;
    }
}

This is how the application layer will look like

Orders

API Layer

Now that everything is ready, let’s make the API. We need to register our mediator, Automapper, and Repository in the program. cs and also the database context.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

// Database connection
var connectionString = builder.Configuration.GetConnectionString("AppDb");
builder.Services.AddDbContext<FOAContext>(x => x.UseSqlServer(connectionString));

// Adding MediatR
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CreateUserHandler>());

// Adding repository
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddTransient<IUserRepository, UserRepository>();
builder.Services.AddTransient<IOrderRepository, OrderRepository>();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

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();

Let's create the Controller.

public class OrderController : Controller
{
    private readonly IMediator _mediator;

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

    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult> Get()
    {
        var result = await _mediator.Send(new GetAllOrderRequest());
        return Ok(result);
    }

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<CreateOrderResponse>> CreateEmployee([FromBody] CreateOrderRequest command)
    {
        var result = await _mediator.Send(command);
        return Ok(result);
    }
}