ASP.NET Core 🟡
Arquitectura y Pipeline
// Program.cs — punto de entrada en .NET 6+
var builder = WebApplication.CreateBuilder(args);
// 1. REGISTRAR SERVICIOS (DI Container)
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IProductoService, ProductoService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* config JWT */ });
var app = builder.Build();
// 2. CONFIGURAR MIDDLEWARE (pipeline de requests)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication(); // ¿quién eres?
app.UseAuthorization(); // ¿qué puedes hacer?
app.MapControllers();
app.Run();
El orden importa: UseAuthentication SIEMPRE antes de UseAuthorization. El middleware se ejecuta en el orden en que se agrega.
Controllers y Actions
[ApiController]
[Route("api/[controller]")]
public class ProductosController : ControllerBase
{
private readonly IProductoService _service;
private readonly ILogger<ProductosController> _logger;
public ProductosController(IProductoService service, ILogger<ProductosController> logger)
{
_service = service;
_logger = logger;
}
// GET api/productos
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductoDto>>> GetAll(
[FromQuery] string? categoria,
[FromQuery] int pagina = 1,
[FromQuery] int porPagina = 10)
{
var productos = await _service.ObtenerAsync(categoria, pagina, porPagina);
return Ok(productos);
}
// GET api/productos/5
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductoDto>> GetById(int id)
{
var producto = await _service.ObtenerPorIdAsync(id);
if (producto is null)
return NotFound(new { mensaje = $"Producto {id} no encontrado" });
return Ok(producto);
}
// POST api/productos
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ProductoDto>> Create([FromBody] CrearProductoDto dto)
{
var producto = await _service.CrearAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = producto.Id }, producto);
}
// PUT api/productos/5
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] ActualizarProductoDto dto)
{
await _service.ActualizarAsync(id, dto);
return NoContent();
}
// DELETE api/productos/5
[HttpDelete("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(int id)
{
await _service.EliminarAsync(id);
return NoContent();
}
}
Model Validation
public class CrearProductoDto
{
[Required(ErrorMessage = "El nombre es obligatorio")]
[StringLength(100, MinimumLength = 3)]
public string Nombre { get; set; } = string.Empty;
[Range(0.01, 999999.99, ErrorMessage = "El precio debe ser entre 0.01 y 999999.99")]
public decimal Precio { get; set; }
[Required]
[Range(1, int.MaxValue)]
public int CategoriaId { get; set; }
}
// [ApiController] valida automáticamente y retorna 400 si hay errores
// También puedes validar manualmente:
if (!ModelState.IsValid)
return BadRequest(ModelState);
Middleware personalizado
// Middleware para logging de requests
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
_logger.LogInformation("Request: {Method} {Path}",
context.Request.Method, context.Request.Path);
await _next(context); // llamar al siguiente middleware
stopwatch.Stop();
_logger.LogInformation("Response: {StatusCode} en {ElapsedMs}ms",
context.Response.StatusCode, stopwatch.ElapsedMilliseconds);
}
}
// Registrar en Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();
Configuración y appsettings
// appsettings.json
{
"ConnectionStrings": {
"Default": "Server=.;Database=MiApp;Trusted_Connection=True;"
},
"Jwt": {
"SecretKey": "mi-clave-super-secreta-larga",
"Issuer": "mi-app",
"Audience": "mi-app-clients",
"ExpirationMinutes": 60
},
"AppSettings": {
"MaxProductosPorPagina": 50,
"AllowedOrigins": ["https://mi-frontend.com"]
}
}
// Leer configuración tipada
public class JwtSettings
{
public string SecretKey { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; } = 60;
}
// Registrar en DI
builder.Services.Configure<JwtSettings>(
builder.Configuration.GetSection("Jwt"));
// Usar en un servicio
public class JwtService
{
private readonly JwtSettings _settings;
public JwtService(IOptions<JwtSettings> options)
{
_settings = options.Value;
}
}
JWT Authentication
// Generar token JWT
public string GenerarToken(Usuario usuario)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_settings.SecretKey));
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, usuario.Id.ToString()),
new Claim(ClaimTypes.Email, usuario.Email),
new Claim(ClaimTypes.Role, usuario.Rol),
};
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_settings.ExpirationMinutes),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Global Exception Handling
// Exception handler middleware centralizado
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>();
var exception = exceptionHandler?.Error;
var (statusCode, mensaje) = exception switch
{
NotFoundException => (StatusCodes.Status404NotFound, exception.Message),
ValidationException => (StatusCodes.Status400BadRequest, exception.Message),
UnauthorizedException => (StatusCodes.Status401Unauthorized, exception.Message),
_ => (StatusCodes.Status500InternalServerError, "Error interno del servidor")
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = mensaje,
traceId = Activity.Current?.Id
});
});
});
Structured Logging con Serilog
Serilog es superior al logging por defecto: propiedades contextuales, múltiples sinks, estructura JSON.
// appsettings.json
{
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "logs/api-.txt",
"rollingInterval": "Day",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId",
"WithProperty"
]
}
}
// Program.cs
builder.Host.UseSerilog((context, config) =>
{
config
.MinimumLevel.Is(LogEventLevel.Information)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Console()
.WriteTo.File("logs/app-.txt", rollingInterval: RollingInterval.Day)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "MiApi")
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName);
});
// En servicios: context variables
public class ProductoService
{
private readonly ILogger<ProductoService> _logger;
public async Task<Producto> CrearAsync(CrearProductoDto dto)
{
using (_logger.BeginScope(new Dictionary<string, object>
{
{ "UsuarioId", usuarioId },
{ "OperationType", "CrearProducto" },
{ "Timestamp", DateTime.UtcNow }
}))
{
_logger.LogInformation("Creando producto: {@Producto}", dto);
try
{
var producto = new Producto { /* ... */ };
await _db.Productos.AddAsync(producto);
await _db.SaveChangesAsync();
_logger.LogInformation("Producto creado: {ProductoId}", producto.Id);
return producto;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al crear producto. {@Dto}", dto);
throw;
}
}
}
}
// Resultado JSON en Seq:
// {
// "Timestamp": "2024-03-27T10:30:45.123Z",
// "Level": "Information",
// "MessageTemplate": "Creando producto: {@Producto}",
// "Properties": {
// "Producto": { "nombre": "Laptop", "precio": 1000 },
// "UsuarioId": 5,
// "OperationType": "CrearProducto",
// "Application": "MiApi"
// }
// }
Filtros (Filters)
Los filtros interceptan requests/responses en puntos específicos del pipeline.
ActionFilter
// Ejecuta antes y después de la acción
public class ValidarModeloFilter : IAsyncActionFilter
{
private readonly ILogger<ValidarModeloFilter> _logger;
public ValidarModeloFilter(ILogger<ValidarModeloFilter> logger)
{
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// ANTES de la acción
if (!context.ModelState.IsValid)
{
_logger.LogWarning("Modelo inválido: {@Errors}", context.ModelState);
context.Result = new BadRequestObjectResult(new
{
errors = context.ModelState.Values.SelectMany(v => v.Errors)
});
return;
}
// DURANTE: ejecutar la acción
var resultContext = await next();
// DESPUÉS de la acción
if (resultContext.Exception == null)
{
_logger.LogInformation("Acción completada exitosamente");
}
}
}
// Registrar en Program.cs
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidarModeloFilter>();
});
// O aplicar a controlador/acción específica
[ServiceFilter(typeof(ValidarModeloFilter))]
[HttpPost("productos")]
public async Task<IActionResult> CrearProducto(CrearProductoDto dto) { }
ExceptionFilter
// Captura excepciones específicas
public class ApiExceptionFilter : IAsyncExceptionFilter
{
private readonly ILogger<ApiExceptionFilter> _logger;
public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
{
_logger = logger;
}
public Task OnExceptionAsync(ExceptionContext context)
{
var exception = context.Exception;
var (statusCode, mensaje) = exception switch
{
NotFoundException notFound => (StatusCodes.Status404NotFound, notFound.Message),
ValidationException validation => (StatusCodes.Status400BadRequest, validation.Message),
UnauthorizedException unauthorized => (StatusCodes.Status401Unauthorized, unauthorized.Message),
ConflictException conflict => (StatusCodes.Status409Conflict, conflict.Message),
_ => (StatusCodes.Status500InternalServerError, "Error interno del servidor")
};
_logger.LogError(exception, "Exception: {@Exception}", new
{
Type = exception.GetType().Name,
Message = exception.Message,
StatusCode = statusCode
});
context.Result = new ObjectResult(new
{
error = mensaje,
traceId = context.HttpContext.TraceIdentifier
})
{
StatusCode = statusCode
};
context.ExceptionHandled = true;
return Task.CompletedTask;
}
}
// Registrar
builder.Services.AddControllers(options =>
{
options.Filters.Add<ApiExceptionFilter>();
});
ResourceFilter
// Ejecuta ANTES de binding y DESPUÉS de result execution
public class LogRequestResponseFilter : IAsyncResourceFilter
{
private readonly ILogger<LogRequestResponseFilter> _logger;
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var request = context.HttpContext.Request;
// ANTES
var body = "";
if (request.Method != "GET")
{
request.EnableBuffering();
body = await new StreamReader(request.Body).ReadToEndAsync();
request.Body.Position = 0;
}
_logger.LogInformation("Request: {Method} {Path}\nBody: {Body}",
request.Method, request.Path, body);
// DURANTE
var resultContext = await next();
// DESPUÉS
_logger.LogInformation("Response: {StatusCode}", resultContext.HttpContext.Response.StatusCode);
}
}
Validación FluentValidation
// Más flexible que DataAnnotations
public class CrearProductoValidator : AbstractValidator<CrearProductoDto>
{
private readonly AppDbContext _db;
public CrearProductoValidator(AppDbContext db)
{
_db = db;
RuleFor(p => p.Nombre)
.NotEmpty().WithMessage("El nombre es requerido")
.Length(3, 100).WithMessage("El nombre debe tener 3-100 caracteres");
RuleFor(p => p.Precio)
.GreaterThan(0).WithMessage("El precio debe ser mayor a 0");
RuleFor(p => p.CategoriaId)
.NotEmpty()
.MustAsync(async (id, ct) =>
{
return await _db.Categorias.AnyAsync(c => c.Id == id, ct);
}).WithMessage("La categoría no existe");
}
}
// Registrar
builder.Services.AddFluentValidation(fv =>
fv.RegisterValidatorsFromAssemblyContaining<Program>());
// En controlador: ASP.NET Core valida automáticamente
[HttpPost]
public async Task<IActionResult> Crear([FromBody] CrearProductoDto dto)
{
// dto ya fue validado, ModelState.IsValid == true
var producto = await _service.CrearAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = producto.Id }, producto);
}
Preguntas frecuentes de entrevista 🎯
1. ¿Qué es el middleware y cómo funciona el pipeline?
El middleware es código que se ejecuta en cada request HTTP, formando una cadena. Cada pieza puede procesar el request, llamar al siguiente middleware, y procesar la response. Se configura en orden y ese orden importa.
2. ¿Cuál es la diferencia entre [Authorize] y [AllowAnonymous]?
[Authorize]requiere que el usuario esté autenticado (y opcionalmente tenga cierto rol/claim).[AllowAnonymous]permite acceso sin autenticación, incluso si hay un[Authorize]global.
3. ¿Cuál es la diferencia entre IActionResult y ActionResult<T>?
ActionResult<T>es más específico y permite que Swagger infiera el tipo de respuesta automáticamente. También permite retornar tanto el tipoTdirectamente como cualquierIActionResult(NotFound, BadRequest, etc.).
4. ¿Qué es CORS y cómo se configura?
CORS (Cross-Origin Resource Sharing) es el mecanismo que permite que un frontend en un dominio distinto haga requests al API.
builder.Services.AddCors(options =>
{
options.AddPolicy("MiPolicy", policy =>
policy.WithOrigins("https://mi-frontend.com")
.AllowAnyMethod()
.AllowAnyHeader());
});
app.UseCors("MiPolicy");
❓ ¿En qué orden se ejecutan los middlewares en ASP.NET Core?