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.
IClienteRepository). Cero dependencias externas (sin EF Core, sin SQL, sin HTTP).▶ 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.EntityFrameworkCoreen 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.Applicationpuede testearse unitariamente mediante mocks deIClienteRepository(usando Moq o NSubstitute) en milisegundos y en memoria, sin requerir conexión a base de datos.