Desglose pedagógico completo: arquitectura, SOLID, autenticación JWT, OWASP Top 10, optimización de consultas, caching y observabilidad. Al finalizar, el estudiante podrá endurecer, optimizar y monitorear una API REST profesional.
1
Arquitectura y separación de responsabilidades
15 min
Objetivo
Comprender patrones arquitectónicos y aplicarlos para lograr bajo acoplamiento y alta cohesión.
// ✅ async hasta el final (async all the way)public async Task<BookDto> GetAsync(int id, CancellationToken ct)
{
var book = await _repo.GetByIdAsync(id, ct);
return book is null ? throw new NotFoundException() : BookDto.FromEntity(book);
}
// ❌ async void (excepto event handlers)public async void DoWork() { }
// ❌ .Result / .Wait() (deadlocks)var book = _repo.GetByIdAsync(id).Result;
// ✅ ConfigureAwait(false) en librerías (no en ASP.NET Core)await _repo.GetByIdAsync(id, ct).ConfigureAwait(false);
3.2.5. Inmutabilidad y records
public recordBookDto(int Id, string Title, string AuthorName);
public recordCreateBookDto(string Title, int AuthorId, string Isbn);
public async Task<BookDto> UpdateAsync(int id, UpdateBookDto dto, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(dto);
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id));
var book = await _repo.GetByIdAsync(id, ct)
?? throw new NotFoundException($"Book {id} no existe");
book.Rename(dto.Title);
await _uow.SaveChangesAsync(ct);
return BookDto.FromEntity(book);
}
3.2.8. Patrones de diseño útiles
Patrón
Aplicación en .NET
Repository
Abstraer persistencia
Unit of Work
Transacción atómica (SaveChanges)
Options
Configuración tipada
Factory
Creación de objetos complejos
Decorator
Añadir logging/caché a servicios
Strategy
Algoritmos intercambiables
Result
Evitar excepciones para flujo normal
Result pattern
public recordResult<T>(bool Success, T? Value, string? Error)
{
public static Result<T> Ok(T value) => new(true, value, null);
public static Result<T> Fail(string error) => new(false, default, error);
}
var result = await _service.CreateAsync(dto, ct);
return result.Success
? CreatedAtRoute(nameof(GetById), new { id = result.Value!.Id }, result.Value)
: BadRequest(new { error = result.Error });
3.2.9. Testing (xUnit + FluentAssertions + Moq)
public classBookServiceTests
{
[Fact]
public async Task CreateAsync_InvalidDto_Throws()
{
var repo = Substitute.For<IBookRepository>(); // NSubstitutevar uow = Substitute.For<IUnitOfWork>();
var validator = new CreateBookValidator();
var sut = new CreateBookHandler(repo, uow, validator);
var dto = new CreateBookDto("", 0, "");
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(dto, default));
}
}
3.2.10. Git y flujo de trabajo
Ramas:main, develop, feature/*, hotfix/*.
Commits atómicos con Conventional Commits: feat:, fix:, chore:, docs:.
3.4.6. Validación de entrada (defensa contra injection)
// ✅ EF Core parametriza automáticamentevar books = await _db.Books.Where(b => b.Title == title).ToListAsync();
// ❌ NUNCA concatenar SQLvar sql = $"SELECT * FROM Books WHERE Title = '{title}'"; // SQL Injection
_db.Books.FromSqlRaw(sql);
// ✅ Si usas SQL raw, parametriza
_db.Books.FromSqlInterpolated($"SELECT * FROM Books WHERE Title = {title}");
if (builder.Environment.IsProduction())
{
var vaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);
builder.Configuration.AddAzureKeyVault(
vaultUri, new DefaultAzureCredential());
}
3.4.11. Cifrado de campos sensibles (EF Core Value Converters)
modelBuilder.Entity<User>()
.Property(u => u.NationalId)
.HasConversion(
v => Encrypt(v),
v => Decrypt(v));
3.4.12. Actualizar dependencias vulnerables
dotnet list package --vulnerable --include-transitive
dotnet list package --outdated
dotnet add package X --version Y
3.4.13. Container hardening (Dockerfile)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Demo.Api.dll"]
3.4.14. Checklist de seguridad
✅ Verificar antes de producción
HTTPS + HSTS.
JWT con clave fuerte y expiración corta.
Contraseñas con BCrypt/Argon2.
CORS restrictivo.
Rate limiting en endpoints sensibles.
Validación de entrada + EF Core parametrizado.
Security headers.
Sin stack traces en producción.
Secretos en Key Vault/User Secrets.
Dependencias auditadas.
Contenedor con usuario no-root.
Logs sin datos sensibles.
⚠ Errores comunes
Devolver ex.ToString() en producción.
Confiar en validación solo del cliente.
CORS abierto "para que funcione".
Rate limiting ausente en login.
Secretos en appsettings.json versionado.
5
Optimización de consultas y operaciones
15 min
Objetivo
Detectar y eliminar cuellos de botella en el acceso a datos.
3.5.1. Reglas de oro EF Core
Regla
Motivo
AsNoTracking() en lecturas
Evita overhead del change tracker
Proyección (Select/ProjectTo)
Trae solo columnas necesarias
AsSplitQuery() con múltiples Include
Evita cartesian explosion
Paginación siempre
No cargar tablas completas
Índices en columnas filtradas
Búsquedas rápidas
Compilar queries calientes
Reduce parsing
3.5.2. Antes y después
// ❌ Antes: carga completa + N+1 + trackingvar orders = await _db.Orders.ToListAsync(); // sin paginarforeach (var o in orders)
{
var customer = await _db.Customers.FindAsync(o.CustomerId); // N+1
Console.WriteLine(customer.Name); // tracking innecesario
}
// ✅ Después: proyección + Include + paginación + no trackingvar result = await _db.Orders
.AsNoTracking()
.Include(o => o.Customer)
.OrderByDescending(o => o.CreatedAt)
.Skip((page - 1) * size)
.Take(size)
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer!.Name,
Total = o.Total
})
.ToListAsync(ct);
3.5.3. Detección de N+1
// Logging de EF Core en Development
options.UseSqlServer(conn)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
private static readonly Func<AppDbContext, int, Task<Book?>> _getBookById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Books.Include(b => b.Author).FirstOrDefault(b => b.Id == id));
var book = await _getBookById(_db, id);
3.5.6. Bulk operations
dotnet add package EFCore.BulkExtensions
await _db.BulkInsertAsync(books); // en lugar de AddRange + SaveChangesawait _db.BulkUpdateAsync(books);
await _db.BulkDeleteAsync(books);
3.5.7. Dapper para queries read-only complejas
dotnet add package Dapper
var sql = @"
SELECT b.Id, b.Title, a.Name AS AuthorName,
COUNT(l.Id) AS LoanCount
FROM Books b
INNER JOIN Authors a ON a.Id = b.AuthorId
LEFT JOIN Loans l ON l.BookId = b.Id
GROUP BY b.Id, b.Title, a.Name
ORDER BY LoanCount DESC";
var report = await _connection.QueryAsync<BookReportDto>(sql);
3.5.8. Proyección con records
var dtos = await _db.Books
.AsNoTracking()
.Select(b => new BookDto(b.Id, b.Title, b.Author!.Name))
.ToListAsync(ct);
3.5.9. Evitar Contains con listas grandes
// ❌ Lento con miles de IDsvar books = await _db.Books.Where(b => ids.Contains(b.Id)).ToListAsync();
// ✅ Mejor: usar temp table o Join con TVP
public classBook
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
[Timestamp]
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}
// En el controladortry { await _db.SaveChangesAsync(ct); }
catch (DbUpdateConcurrencyException)
{
return Conflict(new { error = "El recurso fue modificado por otro usuario." });
}
3.5.12. Checklist de rendimiento
✅ Buenas prácticas
AsNoTracking en GET.
Proyección a DTO.
Paginación + OrderBy indexado.
Índices en columnas filtradas/ordenadas.
Sin N+1 (Include o proyección).
Sin lazy loading.
AsSplitQuery con colecciones múltiples.
Bulk ops para inserciones masivas.
CancellationToken propagado.
Concurrencia optimista donde aplique.
⚠ Errores comunes
ToList() antes de Where.
Count() + ToList() (usar una sola query).
Include de todo "por si acaso".
No usar AsNoTracking.
Faltan índices en FK.
6
Caching y rendimiento
15 min
Objetivo
Aplicar estrategias de caché para reducir latencia y carga.
3.6.1. Niveles de caché
Nivel
Herramienta
Alcance
Velocidad
Memoria (in-process)
IMemoryCache
Una instancia
Muy rápida
Distribuida
IDistributedCache (Redis, SQL)
Múltiples instancias
Rápida
CDN / HTTP
Response caching, ETag
Cliente/proxy
Muy rápida
Output caching
OutputCache (.NET 7+)
Servidor
Rápida
3.6.2. IMemoryCache
builder.Services.AddMemoryCache();
public classBookService
{
private readonly IMemoryCache _cache;
public BookService(IMemoryCache cache) => _cache = cache;
public async Task<BookDto?> GetByIdAsync(int id, CancellationToken ct)
{
var key = $"book:{id}";
if (_cache.TryGetValue(key, out BookDto? cached))
return cached;
var book = await _repo.GetByIdAsync(id, ct);
if (book is null) return null;
var dto = BookDto.FromEntity(book);
_cache.Set(key, dto, TimeSpan.FromMinutes(5));
return dto;
}
}
3.6.3. Caché distribuida (Redis)
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "DemoApp:";
});
public classDistributedBookService
{
private readonly IDistributedCache _cache;
public DistributedBookService(IDistributedCache cache) => _cache = cache;
public async Task<BookDto?> GetByIdAsync(int id, CancellationToken ct)
{
var key = $"book:{id}";
var json = await _cache.GetStringAsync(key, ct);
if (json is not null)
return JsonSerializer.Deserialize<BookDto>(json);
var book = await _repo.GetByIdAsync(id, ct);
if (book is null) return null;
var dto = BookDto.FromEntity(book);
await _cache.SetStringAsync(key, JsonSerializer.Serialize(dto),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}, ct);
return dto;
}
}
3.6.4. Output caching (.NET 7+)
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(b => b.Expire(TimeSpan.FromSeconds(30)));
options.AddPolicy("books", b => b
.Expire(TimeSpan.FromMinutes(5))
.SetVaryByQuery("page", "size")
.Tag("books"));
});
app.UseOutputCache();
[HttpGet]
[OutputCache(PolicyName = "books")]
public async Task<IActionResult> GetAll(int page, int size) { /* ... */ }
3.6.5. Invalidación de caché
public async Task<BookDto> UpdateAsync(int id, UpdateBookDto dto, CancellationToken ct)
{
// ... actualizar en BD
_cache.Remove($"book:{id}");
_cache.Remove("books:all");
return BookDto.FromEntity(book);
}
// Con tags en OutputCache
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, UpdateBookDto dto)
{
// ...await _outputCacheStore.EvictByTagAsync("books", ct);
return NoContent();
}
3.6.6. Estrategias de caché
Estrategia
Cuándo usar
Cache-Aside
Lecturas frecuentes, escrituras esporádicas
Write-Through
Consistencia fuerte requerida
Write-Behind
Alto throughput de escritura
Refresh-Ahead
Datos con TTL predecible
3.6.7. HTTP caching (ETag, Cache-Control)
[HttpGet("{id:int}")]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public async Task<IActionResult> GetById(int id)
{
var book = await _service.GetByIdAsync(id);
if (book is null) return NotFound();
var etag = $"\"{book.Version}\"";
Response.Headers.ETag = etag;
Response.Headers.CacheControl = "public, max-age=60";
if (Request.Headers.IfNoneMatch == etag)
return StatusCode(304);
return Ok(book);
}
⚠ Consideraciones
No cachear datos sensibles (usuarios, tokens).
Definir TTL según volatilidad del dato.
Invalidar en escrituras (o usar TTL corto).
Considerar cache stampede (usar lock o SemaphoreSlim).
Cachear respuestas de error? — generalmente no, o TTL muy corto.
⚠ Errores comunes
Cachear sin TTL (datos obsoletos eternos).
Olvidar invalidar al actualizar.
Cachear respuestas autenticadas en caché compartida.
Cachear datos que cambian constantemente.
Confundir caché de memoria con distribuida en multi-instancia.
7
Diagnóstico y monitoreo
15 min
Objetivo
Instrumentar la aplicación con logs, métricas, tracing y health checks.