Clean Architecture .NET en 2026: CQRS, MediatR y Preguntas de Entrevista para Desarrolladores

Clean Architecture en .NET con CQRS y MediatR 14. Domina los patrones en capas, pipeline behaviors y prepárate para entrevistas de desarrollador senior.

Clean Architecture .NET en 2026: CQRS, MediatR y Preguntas de Entrevista para Desarrolladores

La Clean Architecture en .NET separa la lógica de negocio de las preocupaciones de infraestructura, facilitando el testing, mantenimiento y escalabilidad de las aplicaciones. Combinada con CQRS (Command Query Responsibility Segregation) y MediatR, esta arquitectura proporciona un enfoque estructurado para construir aplicaciones empresariales que los entrevistadores evalúan frecuentemente.

Esencial para Entrevistas

Las preguntas sobre Clean Architecture aparecen en el 70% de las entrevistas para desarrolladores .NET senior. Los entrevistadores esperan que los candidatos expliquen la regla de dependencia: las dependencias apuntan hacia adentro, con la capa de dominio sin ninguna dependencia hacia frameworks externos.

Capas de Clean Architecture y la Regla de Dependencia

El template Ardalis Clean Architecture para ASP.NET Core 10 organiza el código en cuatro capas concéntricas. Cada capa tiene responsabilidades específicas y reglas de dependencia estrictas.

csharp
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerEmail { get; private set; }
    public List<OrderLine> Lines { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }

    // Domain logic encapsulated in the entity
    public void AddLine(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify a submitted order");

        var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
        if (existingLine is not null)
        {
            existingLine.IncreaseQuantity(quantity);
            return;
        }

        Lines.Add(new OrderLine(product.Id, product.Price, quantity));
    }

    public void Submit()
    {
        if (!Lines.Any())
            throw new InvalidOperationException("Cannot submit an empty order");

        Status = OrderStatus.Submitted;
    }
}

La capa de dominio contiene entidades, objetos de valor y servicios de dominio. No tiene conocimiento de bases de datos, HTTP o frameworks externos.

Patrón CQRS: Separando Lecturas de Escrituras

CQRS divide las operaciones en Comandos (operaciones de escritura que cambian el estado) y Consultas (operaciones de lectura que retornan datos). Esta separación permite escalar y optimizar cada camino de forma independiente.

csharp
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;

public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;

public record OrderLineDto(Guid ProductId, int Quantity);

Los comandos retornan datos mínimos, típicamente solo un indicador de éxito o un ID recién creado. Las consultas retornan DTOs optimizados para el consumidor.

src/UseCases/Orders/Queries/GetOrderById/GetOrderByIdQuery.cscsharp
namespace UseCases.Orders.Queries.GetOrderById;

public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;

public record OrderDetailsDto(
    Guid Id,
    string CustomerEmail,
    string Status,
    decimal TotalAmount,
    List<OrderLineDetailsDto> Lines
);

MediatR 14: Pipeline Behaviors e Implementación de Handlers

MediatR 14.2 proporciona la infraestructura de mensajería para CQRS. Cada comando o consulta tiene exactamente un handler, aplicando el principio de responsabilidad única.

src/UseCases/Orders/Commands/CreateOrder/CreateOrderHandler.cscsharp
namespace UseCases.Orders.Commands.CreateOrder;

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Load products to validate and get prices
        var productIds = request.Lines.Select(l => l.ProductId).ToList();
        var products = await _productRepository
            .GetByIdsAsync(productIds, cancellationToken);

        if (products.Count != productIds.Count)
            return Result.NotFound("One or more products not found");

        // Create order using domain logic
        var order = new Order(request.CustomerEmail);

        foreach (var line in request.Lines)
        {
            var product = products.First(p => p.Id == line.ProductId);
            order.AddLine(product, line.Quantity);
        }

        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success(order.Id);
    }
}

¿Listo para aprobar tus entrevistas de .NET?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Pipeline Behaviors para Preocupaciones Transversales

Los pipeline behaviors en MediatR manejan logging, validación, caché y gestión de transacciones sin contaminar los handlers.

src/UseCases/Common/Behaviors/ValidationBehavior.cscsharp
namespace UseCases.Common.Behaviors;

public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Los behaviors se registran en el orden de ejecución deseado. La validación típicamente se ejecuta primero, seguida del logging, luego el manejo de transacciones.

src/Web/Program.cscsharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
    cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
    cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});

Capa de Infraestructura: Implementación del Repository

La capa de infraestructura implementa las interfaces definidas en la capa core. Los repositories de Entity Framework Core traducen las operaciones del dominio a llamadas de base de datos.

src/Infrastructure/Data/Repositories/OrderRepository.cscsharp
namespace Infrastructure.Data.Repositories;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Order?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken = default)
    {
        await _context.Orders.AddAsync(order, cancellationToken);
    }

    public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
        string email,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Where(o => o.CustomerEmail == email)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
}

Preguntas de Entrevista: Lo Que los Desarrolladores Senior Deben Saber

Las entrevistas técnicas para posiciones .NET senior frecuentemente incluyen preguntas sobre Clean Architecture. Estos son los patrones que los entrevistadores evalúan:

P: ¿Por qué la capa de dominio no tiene dependencias hacia frameworks externos?

La capa de dominio contiene reglas de negocio que deben permanecer estables independientemente de los cambios de infraestructura. Si el dominio depende de Entity Framework, cambiar a Dapper u otra base de datos requeriría modificar la lógica de negocio. La regla de dependencia asegura que la infraestructura es un detalle que puede intercambiarse sin afectar el comportamiento central.

P: ¿Cuándo es excesivo usar CQRS?

CQRS agrega complejidad que las aplicaciones CRUD simples no necesitan. Si los modelos de lectura y escritura son casi idénticos, si no hay necesidad de escalado separado, y si el equipo es pequeño, una arquitectura más simple es apropiada. CQRS brilla cuando los modelos de lectura difieren significativamente de los modelos de escritura, cuando se requiere event sourcing, o cuando las cargas de lectura y escritura necesitan escalado independiente.

P: ¿Cómo manejar transacciones a través de múltiples agregados?

csharp
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IUnitOfWork _unitOfWork;

    public TransactionBehavior(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Commands that modify multiple aggregates use explicit transactions
        if (request is not ICommand)
            return await next();

        await using var transaction = await _unitOfWork
            .BeginTransactionAsync(cancellationToken);

        try
        {
            var response = await next();
            await transaction.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}

Para operaciones que abarcan múltiples agregados, utilizar consistencia eventual con eventos de dominio cuando sea posible. Las transacciones sincrónicas a través de agregados indican un posible problema de diseño: los agregados podrían pertenecer juntos.

Optimización del Lado de Lectura con Modelos de Consulta Separados

Las consultas evitan completamente la capa de dominio cuando el rendimiento importa. El acceso directo a la base de datos con Dapper o SQL puro evita la sobrecarga de materializar grafos de entidades completos.

src/Infrastructure/Queries/GetOrderByIdQueryHandler.cscsharp
namespace Infrastructure.Queries;

public class GetOrderByIdQueryHandler
    : IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
    private readonly IDbConnection _connection;

    public GetOrderByIdQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Result<OrderDetailsDto>> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
                   SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
            FROM Orders o
            LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
            WHERE o.Id = @OrderId
            GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
            """;

        var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
            new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));

        return order is null
            ? Result.NotFound($"Order {request.OrderId} not found")
            : Result.Success(order);
    }
}

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Preparación para Entrevistas de Clean Architecture: Puntos Clave

  • La regla de dependencia establece que las dependencias del código fuente solo pueden apuntar hacia adentro. La capa de dominio no tiene conocimiento de bases de datos, frameworks o mecanismos de entrega.
  • CQRS separa comandos (cambios de estado) de consultas (recuperación de datos). Los comandos pasan por el modelo de dominio y validación. Las consultas pueden evitar las entidades del dominio para mejor rendimiento.
  • MediatR 14 enruta las solicitudes a handlers. Los pipeline behaviors manejan preocupaciones transversales como validación, logging y transacciones de manera componible.
  • Las interfaces de repository viven en la capa core. Las implementaciones viven en infraestructura. Esta inversión permite intercambiar EF Core por Dapper sin tocar la lógica de negocio.
  • Las respuestas de entrevista deben incluir compromisos. Clean Architecture agrega complejidad inicial pero se amortiza en aplicaciones grandes y de larga duración con múltiples desarrolladores.
  • El módulo Clean Architecture ASP.NET Core en SharpSkill cubre patrones adicionales incluyendo Specification, objetos Result y Domain Events.
Reto diario

¿Sabrías detectar el bug en .NET?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 15 de septiembre de 2026

Compartir

Artículos relacionados