Validación de datos en APIs mínimas de .NET 10
Las APIs mínimas siempre han sido la alternativa ligera a ASP.NET Core basado en controladores, pero durante mucho tiempo tuvieron una brecha notable: no había soporte integrado para validar los datos entrantes. Debías implementar verificaciones manuales dentro de cada manejador, o dependías de bibliotecas de terceros. .NET 10 llena ese vacío con validación de anotaciones de datos de primera clase para cadenas de consulta, encabezados y cuerpos de solicitud, todo sin paquetes adicionales.
Este análisis profundo de .NET 10 Minimal APIs, basado en el recorrido de Tim Corey, explica cómo registrar el servicio de validación, anotar clases modelo y evitar errores comunes de modificadores de acceso que pueden romper tu configuración.
La Configuración: Una API Mínima Simple
[0:44 - 1:35] La demostración comienza con un proyecto de API Mínima ASP.NET Core muy simple en ejecución en .NET 10. El área de superficie es intencionalmente pequeña: dos puntos finales POST, cada uno aceptando un modelo diferente.
app.MapPost("/person", (Person person) => Results.Ok(person));
app.MapPost("/login", (LoginModel login) => Results.Ok(login));
app.MapPost("/person", (Person person) => Results.Ok(person));
app.MapPost("/login", (LoginModel login) => Results.Ok(login));
El modelo Person lleva los campos de identidad básicos. LoginModel maneja las credenciales: una dirección de correo electrónico, una contraseña, y un campo para confirmar la contraseña. Ambos se envían como cuerpos JSON. En este punto, no hay ninguna verificación de entrada; la API acepta lo que sea que reciba, incluyendo cadenas vacías y direcciones de correo electrónico mal formadas.
Scalar (la moderna interfaz de usuario OpenAPI que viene con .NET 10) se utiliza para enviar solicitudes de prueba directamente desde el navegador, facilitando ver exactamente lo que la API devuelve antes y después de que se conecte la validación.
Registro del Servicio de Validación
[2:36 - 3:06] Antes de que tomen efecto los atributos de validación, necesitas optar por esto a nivel de servicio. Una sola llamada en tu bloque de registro de servicio permite la aplicación en todos los puntos finales de API Mínima:
builder.Services.AddValidation();
builder.Services.AddValidation();
Esa línea es todo el paso de configuración. No hay middleware que agregar, ni etapa de pipeline a la cual engancharse. El framework toma el control automáticamente una vez que el servicio está registrado. Si omites esta llamada, tus atributos de anotación de datos estarán presentes en el modelo pero nunca se evaluarán, y las solicitudes pasan independientemente de lo que contengan.
Esto vale la pena tenerlo en cuenta como primer paso de depuración: si la validación no hace nada en silencio, AddValidation() suele ser la pieza faltante.
Agregando Validación a un Modelo de Clase
[3:00 - 3:55] Con el servicio registrado, agregar validación a un modelo basado en clases es cuestión de decorar propiedades con atributos de anotaciones de datos:
public class Person
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
public class Person
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Después de agregar la directiva de uso System.ComponentModel.DataAnnotations en la parte superior del archivo, marcar FirstName y LastName como [Required] asegura que sean validados. Enviar una solicitud POST con un cuerpo vacío a través de Scalar ahora devuelve un 400 Bad Request con una respuesta de error estructurada:
{
"errors": {
"FirstName": ["The FirstName field is required."],
"LastName": ["The LastName field is required."]
}
}
Sin manejo de errores personalizado, sin atributos de filtro. El framework produce esa respuesta automáticamente, y la verificación se ejecuta antes de que tu cuerpo de manejador se ejecute, por lo que nunca necesitas protegerte contra nulos dentro de la lógica del punto final.
Aplicando Validación a un Registro
[4:29 - 5:30] Los mismos atributos funcionan en records de C#, pero la sintaxis difiere ligeramente porque las propiedades de records generalmente se definen en el constructor principal en lugar de como declaraciones de miembros separadas.
public record LoginModel(
[Required] [EmailAddress] string Email,
[Required] string Password,
[Required] string ConfirmPassword
);
public record LoginModel(
[Required] [EmailAddress] string Email,
[Required] string Password,
[Required] string ConfirmPassword
);
Los atributos en los parámetros del constructor se aplican a las propiedades generadas, por lo que [Required] y [EmailAddress] se comportan exactamente como lo harían en una clase. Enviar una solicitud con un correo electrónico mal formateado, digamos "notanemail", ahora devuelve un 400 que identifica el campo Email como inválido.
El atributo [Compare] también puede ser usado para asegurar que ConfirmPassword coincide con Password. En un record, los objetivos de atributo necesitan un empujón explícito porque el compilador debe saber que te refieres al miembro generado, no al mismo parámetro del constructor:
[property: Compare(nameof(Password))]
string ConfirmPassword
[property: Compare(nameof(Password))]
string ConfirmPassword
El objetivo [property:] le indica al compilador que adjunte el atributo al miembro generado en lugar del parámetro. Sin él, [Compare] compila pero nunca se ejecuta durante la verificación. Esta es la parte más complicada de trabajar con records vs clases en este contexto: las propiedades de clase aceptan atributos naturalmente, mientras que los parámetros de records necesitan el objetivo explícito para cualquier cosa que opere a nivel de miembro.
El Requisito del Modificador de Acceso Público
[7:51 - 9:00] Una trampa común es fácil de pasar por alto y no produce un mensaje de error cuando la alcanzas. El sistema de validación usa reflección para inspeccionar tus tipos de modelo en tiempo de ejecución; para que la reflección encuentre los miembros de un tipo, el tipo mismo debe estar marcado como public.
// Validation will NOT run; the class is internal by default
class Person { ... }
// Validation runs correctly
public class Person { ... }
// Validation will NOT run; the class is internal by default
class Person { ... }
// Validation runs correctly
public class Person { ... }
La misma regla se aplica a los records. Si tu modelo se declara sin un modificador de acceso, C# establece por defecto internal, y el servicio lo omite por completo. Tu punto final aún recibe la solicitud, el manejador aún se ejecuta, y no se devuelve ningún error; las verificaciones simplemente no hacen nada.
Este es un comportamiento de ASP.NET Core, no específico de Minimal APIs, pero aparece más a menudo aquí porque estos proyectos tienden a ser compactos y los desarrolladores a veces definen los modelos en línea o en el mismo archivo que Program.cs sin pensar en la visibilidad.
Una forma rápida de auditar tu proyecto: cualquier tipo de modelo pasado a un manejador de punto final debe estar explícitamente marcado como public. Si no lo está, ningún número de atributos hará que el framework se active.
Qué Puedes Validar
Los atributos de anotación de datos integrados cubren los escenarios más comunes sin ningún trabajo extra:
[Required]rechaza valores nulos o vacíos[EmailAddress]valida el formato de una cadena de correo electrónico[Compare]verifica que dos propiedades coincidan, útil para la confirmación de contraseñas[Range]impone límites numéricos o de fecha[StringLength]limita la longitud de la cadena con un mínimo opcional[RegularExpression]valida contra un patrón personalizado
Todos estos funcionan a través de parámetros de cadena de consulta, encabezados de solicitud y cuerpos JSON. La misma clase de modelo o registro se puede vincular desde diferentes fuentes sin cambiar ninguno de sus atributos.
Conclusión
[7:46 - end] Con AddValidation() registrado y tus modelos decorados, Minimal APIs imponen restricciones de entrada automáticamente en clases y registros por igual. Para que esto funcione en .NET 10, simplemente llama a builder.Services.AddValidation() una vez, decora las propiedades de tu modelo con atributos de anotación de datos estándar, y asegúrate de que cada tipo de modelo esté marcado como public.
El objetivo [property:] en registros y el requisito del modificador public son los únicos verdaderos inconvenientes. Son fáciles de pasar por alto y producen fallos silenciosos en lugar de errores de compilación, así que mantenlos en tu lista de verificación cuando las verificaciones de entrada parezcan no hacer nada.
Mira el video completo en el canal de YouTube de Tim Corey para el recorrido completo del código fuente.
