← maurobernal.com.ar

Validacion de Datos en .NET 10 con Data Annotations: Guia Completa

Validar los datos de entrada es una de las primeras líneas de defensa de cualquier API. En .NET, la forma más directa de hacerlo es con Data Annotations: atributos que se aplican sobre las propiedades del modelo y que ASP.NET Core evalúa automáticamente antes de que el request llegue a tu lógica de negocio.

En esta primera parte repasamos en profundidad cómo funciona Data Annotations en .NET 10, sus capacidades y sus límites. En la segunda parte comparamos esta estrategia contra FluentValidation.

¿Qué son los Data Annotations?

Los Data Annotations son atributos del namespace System.ComponentModel.DataAnnotations que se aplican directamente sobre las propiedades de un modelo para declarar reglas de validación. ASP.NET Core los evalúa durante el model binding y popula automáticamente ModelState con los errores encontrados.

using System.ComponentModel.DataAnnotations;

public class CreateUserRequest
{
    [Required(ErrorMessage = "El email es obligatorio.")]
    [EmailAddress(ErrorMessage = "El formato del email no es válido.")]
    [MaxLength(150)]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "El nombre es obligatorio.")]
    [StringLength(80, MinimumLength = 2, ErrorMessage = "El nombre debe tener entre 2 y 80 caracteres.")]
    public string Name { get; set; } = string.Empty;

    [Range(13, 120, ErrorMessage = "La edad debe estar entre 13 y 120.")]
    public int Age { get; set; }
}

Con este modelo, ASP.NET Core valida automáticamente cada request antes de ejecutar el action method.

Integración automática en ASP.NET Core (Controllers)

En una aplicación MVC o Web API con controllers, la validación es completamente automática. Solo tenés que verificar ModelState.IsValid:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
    {
        // Si los atributos fallan, ASP.NET Core devuelve 400 automáticamente
        // (gracias al atributo [ApiController])

        // ... lógica de negocio
        return Created($"/api/users/{Guid.NewGuid()}", request);
    }
}

El atributo [ApiController] activa la validación automática del ModelState: si alguna regla falla, el framework devuelve un 400 Bad Request con el detalle de los errores sin que tengas que escribir código adicional.

Integración en Minimal APIs (.NET 10)

En Minimal APIs, la validación no es automática. En .NET 10 se puede activar de forma explícita con el nuevo filtro integrado:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/users", async (CreateUserRequest request) =>
{
    // La validación de anotaciones se puede activar con un endpoint filter
    return Results.Created($"/users/{Guid.NewGuid()}", request);
})
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();

O implementando un filtro genérico reutilizable:

public class ValidationFilter<T> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
    {
        var model = ctx.Arguments.OfType<T>().FirstOrDefault();
        if (model is null) return Results.BadRequest("Payload requerido.");

        var context = new ValidationContext(model);
        var results = new List<ValidationResult>();

        if (!Validator.TryValidateObject(model, context, results, validateAllProperties: true))
        {
            var errors = results
                .GroupBy(r => r.MemberNames.FirstOrDefault() ?? "General")
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(r => r.ErrorMessage!).ToArray()
                );

            return Results.ValidationProblem(errors);
        }

        return await next(ctx);
    }
}

Atributos más usados

Atributo Descripción
[Required] El campo no puede ser null ni vacío
[StringLength(max, MinimumLength = min)] Longitud de string entre mín y máx
[MaxLength(n)] / [MinLength(n)] Longitud máxima o mínima
[Range(min, max)] Valor numérico dentro de un rango
[EmailAddress] Formato de email válido
[Url] Formato de URL válida
[Phone] Formato de número telefónico
[RegularExpression(pattern)] Validación con expresión regular
[Compare("OtherProp")] Compara el valor con otra propiedad
[CreditCard] Número de tarjeta de crédito válido
[AllowedValues(...)] Valor dentro de un conjunto permitido (.NET 8+)
[DeniedValues(...)] Valor fuera de un conjunto denegado (.NET 8+)
[Length(min, max)] Longitud de colecciones o strings (.NET 8+)
[Base64String] String Base64 válido (.NET 8+)

Validaciones personalizadas

Opción 1: IValidatableObject

Para lógica que involucra múltiples propiedades del mismo modelo:

public class CreateUserRequest : IValidatableObject
{
    public string? Email { get; set; }
    public int Age { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Age < 18 && Email?.EndsWith(".work") == true)
            yield return new ValidationResult(
                "Los menores no pueden registrarse con un email de trabajo.",
                new[] { nameof(Email) }
            );
    }
}

Opción 2: Atributo personalizado

Para reglas reutilizables en múltiples modelos:

public class NoDisposableEmailAttribute : ValidationAttribute
{
    private static readonly string[] _blocked = { "test@test.com", "lorem@ipsum.com" };

    protected override ValidationResult? IsValid(object? value, ValidationContext context)
    {
        if (value is string email && _blocked.Contains(email.ToLower()))
            return new ValidationResult("Usá un email real, no uno descartable.");

        return ValidationResult.Success;
    }
}

// Uso
[NoDisposableEmail]
public string Email { get; set; } = string.Empty;

Respuesta de error: ProblemDetails

Con [ApiController], los errores de validación se devuelven automáticamente como ValidationProblemDetails (RFC 7807):

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Email": ["El formato del email no es válido."],
    "Name": ["El nombre es obligatorio."]
  }
}

Limitaciones de Data Annotations

  • No admiten async: no podés consultar la base de datos para verificar si un email ya existe dentro de un atributo.
  • La lógica se mezcla con el modelo: el DTO carga con responsabilidades de validación.
  • Difícil de testear de forma aislada: los atributos no son clases independientes fácilmente instanciables.
  • Reglas cruzadas son engorrosas: IValidatableObject funciona pero no escala bien con lógica compleja.
  • Mensajes no localizables fácilmente: los ErrorMessage son strings literales (aunque se pueden usar resource files).
  • No hay soporte para RuleSets: no podés tener distintas reglas para «crear» vs «actualizar» el mismo modelo.

¿Cuándo usar Data Annotations?

  • Proyectos pequeños o medianos con reglas simples.
  • Equipos que prefieren cero dependencias externas.
  • Modelos con validaciones estáticas y directas (no async, no cruzadas).
  • Cuando la validación vive cómodamente junto al modelo (DTOs simples).

Conclusión

Data Annotations es el sistema de validación nativo de .NET: sin dependencias, integración automática con el pipeline de ASP.NET Core, y atributos suficientes para cubrir la mayoría de los casos comunes. En .NET 10, el conjunto de atributos disponibles creció significativamente con adiciones como [AllowedValues], [DeniedValues], [Length] y [Base64String].

Sus limitaciones aparecen cuando las reglas de validación se vuelven complejas: lógica async, múltiples propiedades correlacionadas, o la necesidad de distintas reglas para distintos contextos. Para esos escenarios, la respuesta es FluentValidation. Lo vemos en la segunda parte de esta serie.