BackendAdvanced
20 min readNov 24, 2025
Clean Architecture in .NET 8 with Real Examples
Implement clean architecture patterns in .NET 8 with entities, use cases, and dependency injection.
R
Rithy Tep
Author
Project Structure
src/
├── Domain/ # Entities, Value Objects
├── Application/ # Use Cases, Interfaces
├── Infrastructure/ # Data Access, External Services
└── API/ # Controllers, Middleware
Domain Layer - Entities
// Domain/Entities/Order.cs public class Order : BaseEntity { public string OrderNumber { get; private set; } public decimal TotalAmount { get; private set; } public OrderStatus Status { get; private set; } public List<OrderItem> Items { get; private set; } = new(); public static Order Create(string customerId, List<OrderItem> items) { var order = new Order { OrderNumber = GenerateOrderNumber(), Status = OrderStatus.Pending, Items = items }; order.CalculateTotal(); return order; } public void ConfirmOrder() { if (Status != OrderStatus.Pending) throw new InvalidOperationException("Order already processed"); Status = OrderStatus.Confirmed; } private void CalculateTotal() { TotalAmount = Items.Sum(item => item.Price * item.Quantity); } }
Application Layer - Use Cases
// Application/Orders/Commands/CreateOrderCommand.cs public record CreateOrderCommand( string CustomerId, List<OrderItemDto> Items ) : IRequest<OrderDto>; public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderDto> { private readonly IOrderRepository _orderRepository; private readonly IUnitOfWork _unitOfWork; public async Task<OrderDto> Handle( CreateOrderCommand request, CancellationToken cancellationToken) { var items = request.Items.Select(dto => new OrderItem(dto.ProductId, dto.Quantity, dto.Price) ).ToList(); var order = Order.Create(request.CustomerId, items); await _orderRepository.AddAsync(order); await _unitOfWork.SaveChangesAsync(cancellationToken); return order.ToDto(); } }
Infrastructure Layer - Repository
// Infrastructure/Repositories/OrderRepository.cs public class OrderRepository : IOrderRepository { private readonly AppDbContext _context; public async Task<Order?> GetByIdAsync(Guid id) { return await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id); } public async Task AddAsync(Order order) { await _context.Orders.AddAsync(order); } }
API Layer - Controller
[ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; [HttpPost] public async Task<ActionResult<OrderDto>> CreateOrder( CreateOrderCommand command) { var result = await _mediator.Send(command); return CreatedAtAction(nameof(GetOrder), new { id = result.Id }, result); } [HttpGet("{id}")] public async Task<ActionResult<OrderDto>> GetOrder(Guid id) { var query = new GetOrderQuery(id); var result = await _mediator.Send(query); return result ?? NotFound(); } }
Dependency Injection Setup
// Program.cs builder.Services.AddScoped<IOrderRepository, OrderRepository>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly) );
Benefits
- •Testability: Mock interfaces easily
- •Maintainability: Clear separation of concerns
- •Flexibility: Swap implementations without changing business logic
#.NET#C##Clean Architecture#CQRS#DDD