El fichero, entero
Esto es exactamente lo que descargas: sin resúmenes ni recortes.
.NET: API REST mantenible
Convenciones para una API en .NET 8 que se pueda leer dentro de seis meses.
Estructura
src/
Api/ → endpoints, DI, middleware
Application/ → casos de uso, DTOs, validadores
Domain/ → entidades y reglas de negocio
Infrastructure/ → DbContext, repositorios, servicios externos
La dependencia apunta siempre hacia dentro: Api → Application → Domain.
Infrastructure implementa interfaces declaradas en Application.
Endpoints
Minimal APIs agrupadas por recurso, nunca un Program.cs de 400 líneas:
public static class ProjectsEndpoints
{
public static RouteGroupBuilder MapProjects(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/projects").WithTags("Projects");
group.MapGet("/", GetAll);
group.MapGet("/{id:guid}", GetById).WithName(nameof(GetById));
group.MapPost("/", Create).RequireAuthorization();
return group;
}
private static async Task<Results<Ok<ProjectDto>, NotFound>> GetById(
Guid id, IProjectService service, CancellationToken ct)
{
var project = await service.GetAsync(id, ct);
return project is null ? TypedResults.NotFound() : TypedResults.Ok(project);
}
}
TypedResultssobreResults: documenta el contrato en OpenAPI sin atributos extra.CancellationTokenen toda operación async que llegue a base de datos.- La entidad de dominio nunca cruza el límite HTTP: se mapea a DTO.
EF Core
var projects = await _db.Projects
.AsNoTracking() // solo lectura → sin change tracker
.Where(p => p.Status == status)
.OrderBy(p => p.Order)
.Select(p => new ProjectDto(p.Id, p.Title, p.Url)) // proyección en SQL
.ToListAsync(ct);
Checklist antes de dar por buena una consulta:
-
AsNoTracking()en lecturas. - Proyección con
Selectantes de materializar: no traigas la entidad entera para usar tres columnas. - Sin N+1:
Includeexplícito o proyección; revísalo activando el log de SQL en desarrollo. - Índice en toda columna usada para filtrar u ordenar.
- Migraciones revisadas a mano antes de aplicarlas a producción.
Errores y validación
- Un
IExceptionHandlerglobal que devuelve ProblemDetails (RFC 7807). Nada detry/catchrepetido en cada endpoint. - Validación con FluentValidation en el borde, no dentro del dominio.
- Nunca devuelvas el mensaje de la excepción al cliente: registra el detalle, responde con un identificador de correlación.
Configuración
- Secretos en User Secrets (desarrollo) o Key Vault (producción). Jamás en
appsettings.jsonversionado. IOptions<T>con validación al arrancar (ValidateOnStart): mejor fallar en el despliegue que en la primera petición.