Clean Architecture & Design Patterns
Production .NET applications use architectural patterns to manage complexity — Clean Architecture separates concerns into layers, CQRS splits reads from writes, and the Repository pattern abstracts data access.
40 min•By Priygop Team•Last updated: Feb 2026
Architectural Patterns
- Clean Architecture — Domain (entities) → Application (use cases) → Infrastructure (database, APIs) → Presentation (controllers). Inner layers have no dependencies on outer layers
- Repository Pattern — IRepository<T> abstracts data access. Implementation uses Entity Framework. Controllers depend on interfaces, not EF directly
- Unit of Work — Groups multiple repository operations into a single transaction. IUnitOfWork with SaveChangesAsync()
- CQRS — Separate models for reads (queries) and writes (commands). Commands modify state; queries return data. Use MediatR for handler dispatch
- Mediator Pattern — MediatR library. Controllers send IRequest objects; handlers process them. Decouples controllers from business logic
- Domain-Driven Design — Entities (identity), Value Objects (no identity), Aggregates (consistency boundary), Domain Events
.NET Architecture Code
Example
// Repository interface (Application layer)
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
// EF Core implementation (Infrastructure layer)
public class Repository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
return entity;
}
}
// CQRS with MediatR
public record GetUserQuery(int Id) : IRequest<UserDto>;
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto>
{
private readonly IRepository<User> _repo;
public GetUserHandler(IRepository<User> repo) => _repo = repo;
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct)
{
var user = await _repo.GetByIdAsync(request.Id)
?? throw new NotFoundException("User not found");
return new UserDto(user.Id, user.Name, user.Email);
}
}
// Controller using MediatR
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator) => _mediator = mediator;
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> Get(int id)
{
return Ok(await _mediator.Send(new GetUserQuery(id)));
}
}