.NET & C#
🤖Generado & Curado por IA (Google Gemini 2.0)
5 min de lectura

Arquitectura en Capas en .NET C#: Guía para Iniciar un Proyecto

Estructuración limpia de soluciones en .NET C# mediante modelo de capas (Domain, Application, Infrastructure, API) con inyección de dependencias.

Iniciar un proyecto empresarial en .NET C# sin una separación clara de responsabilidades es la vía más rápida hacia la deuda técnica y el código espagueti. Cuando la lógica de acceso a datos (queries o llamadas a Entity Framework) se mezcla dentro de los controladores de la API, el sistema se vuelve frágil, difícil de testear y prácticamente imposible de refactorizar.

El modelo de arquitectura en capas (N-Layer Architecture) organiza la solución desacoplando el núcleo del negocio de los detalles de infraestructura (base de datos, protocolos y librerías externas), aplicando el principio de Inversión de Dependencias (DIP).

A continuación, la guía práctica y técnica para estructurar una solución robusta desde cero en .NET.


1. Las 4 Capas Fundamentales y el Flujo de Dependencias

La regla dorada de esta arquitectura es que las dependencias de código siempre apuntan hacia adentro: las capas externas conocen a las internas, pero el núcleo del negocio (Domain / Core) no depende de ningún componente externo, motor de base de datos ni framework web.

📐 Arquitectura en Capas: Secuencia de Ejecución y Dependencias
La petición HTTP viaja de arriba hacia abajo (1 → 4), mientras que la Inversión de Dependencias (DIP) desacopla el núcleo del negocio de la base de datos y la infraestructura técnica.
🌐 1. Capa de Presentación (WebAPI)
Punto de Entrada HTTP
Recibe las peticiones HTTP externas (JSON/REST), valida el formato de entrada, gestiona autenticación JWT y delega la ejecución al caso de uso correspondiente. No contiene reglas de negocio ni interactúa con la base de datos.
Controladores / Minimal APIs Middlewares DTOs Entrada/Salida Swagger / OpenAPI
[Paso 1 → 2] Invoca Casos de Uso (Pasa DTOs) • Depende de Application
⚙️ 2. Capa de Aplicación (Application)
Orquestación & Casos de Uso
Coordina los flujos de trabajo del sistema. Ejecuta validaciones de aplicación, coordina el ciclo de vida de entidades e interactúa con el Dominio y los contratos de repositorio (interfaces).
Casos de Uso / Servicios DTOs & Mappers FluentValidation MediatR / CQRS
[Paso 2 → 3] Aplica Reglas de Negocio • Depende de Domain
🛡️ 3. Capa de Dominio (Domain / Core)
★ Núcleo Agnóstico (C# Puro)
El corazón de la solución. Contiene las entidades, value objects y reglas invariantes del negocio. Define las interfaces de abstracción (IClienteRepository). Cero dependencias externas (sin EF Core, sin SQL, sin HTTP).
Entidades POCO Value Objects Interfaces de Repo Domain Exceptions
[Paso 3 ↔ 4] Inversión de Dependencias (DIP) • Infraestructura implementa contratos del Core
🔌 4. Capa de Infraestructura (Infrastructure)
Persistencia & Servicios Externos
Maneja los detalles técnicos y conexiones externas. Implementa las interfaces de persistencia definidas en el Dominio usando ORMs, escribe queries SQL, consume APIs externas y gestiona cachés.
DbContext (EF Core) Repositorios Concretos Dapper / Migraciones HTTP Clients Externos
[Paso 4 → BD] Ejecuta Queries y Transacciones SQL
🗄️ Base de Datos & Almacenamiento (SQL Server / PostgreSQL / Redis)
Motor de Persistencia
▶ Ver diagrama en formato ASCII / Terminal (Para copiar a README o documentación interna)
+---------------------------------------------------------------+
|                 1. PRESENTATION (Capa WebAPI)                 |
|        Controladores, Endpoints, Middlewares, Filtros         |
+---------------------------------------------------------------+
                                |
                                | [1] Invoca Casos de Uso (DTOs)
                                v (Depende de Application)
+---------------------------------------------------------------+
|                2. APPLICATION (Casos de Uso)                  |
|       Orquestación, DTOs, Mappers, Validaciones, CQRS         |
+---------------------------------------------------------------+
                                |
                                | [2] Aplica Reglas de Negocio
                                v (Depende de Domain)
+---------------------------------------------------------------+
|                  3. DOMAIN / CORE (Núcleo)                    |
|         Entidades, Value Objects, Interfaces de Repo          |
+---------------------------------------------------------------+
                                |
                                | [3] Inversión de Dependencias
                                v (Infrastructure implementa IRepo)
+---------------------------------------------------------------+
|               4. INFRASTRUCTURE (Persistencia)                |
|       DbContext (EF Core / Dapper), Repositorios, HTTP        |
+---------------------------------------------------------------+
                                |
                                | [4] Queries y Transacciones SQL
                                v
+---------------------------------------------------------------+
|        BASE DE DATOS (SQL Server / PostgreSQL / Redis)        |
+---------------------------------------------------------------+

Resumen de Responsabilidades por Capa:

Capa Rol Principal Dependencias Directas Lo que NUNCA debe contener
1. Presentation Endpoints HTTP, serialización, middlewares, Swagger y autenticación Application e Infrastructure (solo en IoC / Program.cs) Consultas SQL, lógica de negocio o entidades directas de BD.
2. Application Casos de uso, orquestación, DTOs, mappers y validaciones Domain Controladores, SQL directo, dependencias de UI o paquetes de BD.
3. Domain / Core Entidades de negocio (POCOs), Value Objects y contratos de repositorios Ninguna (C# Puro) Paquetes de base de datos, EF Core, HTTP, JSON o librerías de UI.
4. Infrastructure Implementación de repositorios, DbContext, migraciones y servicios externos Domain y Application Lógica de presentación, controladores HTTP o vistas.

2. Ejemplo Práctico: El Flujo de Código a través de las Capas

Para entender la separación real, observemos cómo fluye la creación y consulta de un Cliente:

A. Capa Domain: Entidad pura e interfaz del repositorio

// src/MiProyecto.Domain/Entities/Cliente.cs
namespace MiProyecto.Domain.Entities;

public class Cliente
{
    public Guid Id { get; private set; }
    public string Nombre { get; private set; }
    public string Email { get; private set; }
    public bool Activo { get; private set; }

    public Cliente(string nombre, string email)
    {
        Id = Guid.NewGuid();
        Nombre = nombre;
        Email = email;
        Activo = true;
    }

    public void Desactivar() => Activo = false;
}

// src/MiProyecto.Domain/Interfaces/IClienteRepository.cs
namespace MiProyecto.Domain.Interfaces;

public interface IClienteRepository
{
    Task<Cliente?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(Cliente cliente, CancellationToken ct = default);
}

B. Capa Application: DTOs, Contratos y Casos de Uso (Servicio)

// src/MiProyecto.Application/DTOs/ClienteDtos.cs
namespace MiProyecto.Application.DTOs;

public record ClienteResponseDto(Guid Id, string Nombre, string Email, bool Activo);
public record CreateClienteRequestDto(string Nombre, string Email);

// src/MiProyecto.Application/Services/IClienteService.cs
namespace MiProyecto.Application.Services;

public interface IClienteService
{
    Task<ClienteResponseDto?> ObtenerPorIdAsync(Guid id, CancellationToken ct = default);
    Task<ClienteResponseDto> CrearAsync(CreateClienteRequestDto dto, CancellationToken ct = default);
}

// src/MiProyecto.Application/Services/ClienteService.cs
using MiProyecto.Application.DTOs;
using MiProyecto.Domain.Entities;
using MiProyecto.Domain.Interfaces;

namespace MiProyecto.Application.Services;

public class ClienteService : IClienteService
{
    private readonly IClienteRepository _clienteRepository;

    public ClienteService(IClienteRepository clienteRepository)
    {
        _clienteRepository = clienteRepository;
    }

    public async Task<ClienteResponseDto?> ObtenerPorIdAsync(Guid id, CancellationToken ct = default)
    {
        var cliente = await _clienteRepository.GetByIdAsync(id, ct);
        return cliente is not null 
            ? new ClienteResponseDto(cliente.Id, cliente.Nombre, cliente.Email, cliente.Activo)
            : null;
    }

    public async Task<ClienteResponseDto> CrearAsync(CreateClienteRequestDto dto, CancellationToken ct = default)
    {
        var cliente = new Cliente(dto.Nombre, dto.Email);
        await _clienteRepository.AddAsync(cliente, ct);
        return new ClienteResponseDto(cliente.Id, cliente.Nombre, cliente.Email, cliente.Activo);
    }
}

// src/MiProyecto.Application/DependencyInjection.cs
using Microsoft.Extensions.DependencyInjection;
using MiProyecto.Application.Services;

namespace MiProyecto.Application;

public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddScoped<IClienteService, ClienteService>();
        return services;
    }
}

C. Capa Infrastructure: Implementación con EF Core

// src/MiProyecto.Infrastructure/Repositories/ClienteRepository.cs
using Microsoft.EntityFrameworkCore;
using MiProyecto.Domain.Entities;
using MiProyecto.Domain.Interfaces;
using MiProyecto.Infrastructure.Persistence;

namespace MiProyecto.Infrastructure.Repositories;

public class ClienteRepository : IClienteRepository
{
    private readonly AppDbContext _context;

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

    public async Task<Cliente?> GetByIdAsync(Guid id, CancellationToken ct = default)
        => await _context.Clientes.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id, ct);

    public async Task AddAsync(Cliente cliente, CancellationToken ct = default)
    {
        await _context.Clientes.AddAsync(cliente, ct);
        await _context.SaveChangesAsync(ct);
    }
}

D. Capa Presentation (WebAPI): Controlador HTTP Limpio

// src/MiProyecto.Api/Controllers/ClientesController.cs
using Microsoft.AspNetCore.Mvc;
using MiProyecto.Application.DTOs;
using MiProyecto.Application.Services;

namespace MiProyecto.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ClientesController : ControllerBase
{
    private readonly IClienteService _clienteService;

    public ClientesController(IClienteService clienteService)
    {
        _clienteService = clienteService;
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var cliente = await _clienteService.ObtenerPorIdAsync(id, ct);
        return cliente is not null ? Ok(cliente) : NotFound();
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateClienteRequestDto dto, CancellationToken ct)
    {
        var nuevo = await _clienteService.CrearAsync(dto, ct);
        return CreatedAtAction(nameof(GetById), new { id = nuevo.Id }, nuevo);
    }
}

3. Inicialización de la Solución desde la Terminal (CLI)

No requieres abrir Visual Studio para crear esta estructura limpia. Puedes bootstrappear toda la solución mediante comandos estándar de la CLI de .NET:

# 1. Crear carpeta del proyecto y solución general
mkdir MiProyectoEmpresarial && cd MiProyectoEmpresarial
dotnet new sln -n MiProyectoEmpresarial

# 2. Crear las librerías de clases para cada capa dentro de /src
dotnet new classlib -o src/MiProyecto.Domain
dotnet new classlib -o src/MiProyecto.Application
dotnet new classlib -o src/MiProyecto.Infrastructure
dotnet new webapi -o src/MiProyecto.Api

# 3. Vincular todos los proyectos a la solución
dotnet sln add src/**/*.csproj

# 4. Configurar las referencias entre proyectos (Inversión de Dependencias)
# Application depende únicamente de Domain
dotnet add src/MiProyecto.Application reference src/MiProyecto.Domain

# Infrastructure implementa interfaces de Domain y Application
dotnet add src/MiProyecto.Infrastructure reference src/MiProyecto.Domain
dotnet add src/MiProyecto.Infrastructure reference src/MiProyecto.Application

# Api referencia Application (para lógica) e Infrastructure (para Inyección de Dependencias)
dotnet add src/MiProyecto.Api reference src/MiProyecto.Application
dotnet add src/MiProyecto.Api reference src/MiProyecto.Infrastructure

4. El Pegamento Arquitectónico: Inyección de Dependencias (DI)

Para mantener desacopladas la API y la Infraestructura, es una excelente práctica encapsular el registro de servicios dentro de un método de extensión en la capa correspondiente:

// src/MiProyecto.Infrastructure/DependencyInjection.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MiProyecto.Domain.Interfaces;
using MiProyecto.Infrastructure.Persistence;
using MiProyecto.Infrastructure.Repositories;

namespace MiProyecto.Infrastructure;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));

        services.AddScoped<IClienteRepository, ClienteRepository>();
        return services;
    }
}

Y en el punto de entrada de la API (Program.cs):

// src/MiProyecto.Api/Program.cs
using MiProyecto.Application;
using MiProyecto.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

// Registro modular de servicios por capa
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

5. Antipatrones Críticos a Evitar en Producción

  • Antipatrón “Entity Leak”: Jamás expongas las entidades de base de datos (Cliente) en las respuestas de los controladores. Expón siempre DTOs (ClienteResponseDto). Exponer la entidad acopla el contrato público del API a la estructura de la base de datos y arriesga fugas de campos sensibles (hashes, contraseñas, flags de auditoría interna).
  • Contaminación del Dominio: Si observas using Microsoft.EntityFrameworkCore en un archivo de la capa Domain, detén la ejecución: estás acoplando tu lógica de negocio al ORM de turno.
  • Testabilidad Aislada: Con este desacoplamiento, la capa MiProyecto.Application puede testearse unitariamente mediante mocks de IClienteRepository (usando Moq o NSubstitute) en milisegundos y en memoria, sin requerir conexión a base de datos.
AS

Alejandro Alfonso Salazar Chaves

M.Sc. en Ciencias de la Computación (Universidad de Costa Rica) con más de 20 años de experiencia en desarrollo de software de misión crítica, infraestructura y soporte en Data Center (Racknation) y arquitectura de sistemas en PHPBEAST.

✓ Copiado al portapapeles