Validación de modelos en ASP.NET Core MVC y Razor Pages

En este artículo se explica cómo validar la entrada del usuario en una aplicación ASP.NET Core MVC o Razor Pages.

Vea o descargue el código de ejemplo (cómo descargarlo).

Estado del modelo

El estado del modelo representa los errores que proceden de dos subsistemas: el enlace de modelos y la validación de modelos. Los errores que se originan del enlace de modelos suelen ser errores de conversión de datos. Por ejemplo, se escribe una "x" en un campo numérico entero. La validación del modelo se produce después del enlace de modelos y notifica los errores en los que los datos no cumplen las reglas de negocio. Por ejemplo, se especifica un 0 en un campo que espera una clasificación entre 1 y 5.

Tanto el enlace como la validación de modelos se producen antes de la ejecución de una acción de controlador o un método de controlador de Razor Pages. En el caso de las aplicaciones web, la aplicación es responsable de inspeccionar ModelState.IsValid y reaccionar de manera apropiada. Normalmente, las aplicaciones web vuelven a reproducir la página con un mensaje de error, como se muestra en el ejemplo de Razor Pages siguiente:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Para ASP.NET Core MVC con controladores y vistas, en el ejemplo siguiente se muestra cómo revisar ModelState.IsValid dentro de una acción de controlador:

public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}

Los controladores de Web API no tienen que comprobar si ModelState.IsValid tienen el atributo [ApiController]. En ese caso, se devuelve una respuesta HTTP 400 automática que contiene los detalles del error cuando el estado del modelo no es válido. Para obtener más información, consulte Respuestas HTTP 400 automáticas.

Nueva ejecución de la validación

La validación es automática, pero tal vez le interese repetirla manualmente. Por ejemplo, tal vez haya calculado el valor de una propiedad y quiera volver a ejecutar la validación después de establecer la propiedad en el valor calculado. Para volver a ejecutar la validación, llame a ModelStateDictionary.ClearValidationState para borrar la validación específica del modelo que se está validando seguida de TryValidateModel:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Atributos de validación

Los atributos de validación permiten especificar reglas de validación para las propiedades del modelo. En el ejemplo siguiente de la aplicación de ejemplo se muestra una clase de modelo anotada con atributos de validación. El atributo [ClassicMovie] es un atributo de validación personalizado y los demás están integrados. No se muestra [ClassicMovieWithClientValidator], que indica una manera alternativa de implementar un atributo personalizado.

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

Atributos integrados

Estos son algunos de los atributos de validación integrados:

  • [ValidateNever]: indica que una propiedad o parámetro se debe excluir de la validación.
  • [CreditCard]: valida que la propiedad tenga formato de tarjeta de crédito. Requiere métodos adicionales de validación de jQuery.
  • [Compare]: valida que dos propiedades de un modelo coincidan.
  • [EmailAddress]: valida que la propiedad tenga formato de correo electrónico.
  • [Phone]: valida que la propiedad tenga formato de número de teléfono.
  • [Range]: valida que el valor de propiedad se encuentre dentro de un intervalo especificado.
  • [RegularExpression]: valida que el valor de propiedad coincida con una expresión regular especificada.
  • [Required]: valida que el campo no es NULL. Consulte el atributo [Required] para obtener más información sobre el comportamiento de este atributo.
  • [StringLength]: valida que un valor de propiedad de cadena no supere un límite de longitud especificado.
  • [Url]: valida que la propiedad tenga un formato de URL.
  • [Remote]: valida la entrada en el cliente mediante una llamada a un método de acción en el servidor. Consulte el atributo [Remote] para obtener más información sobre el comportamiento de este atributo.

En el espacio de nombres System.ComponentModel.DataAnnotations encontrará una lista completa de atributos de validación.

Mensajes de error

Los atributos de validación permiten especificar el mensaje de error que se mostrará para una entrada no válida. Por ejemplo:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

Internamente, los atributos llaman a String.Format con un marcador de posición para el nombre de campo y, en ocasiones, marcadores de posición adicionales. Por ejemplo:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

Cuando se aplica a una propiedad Name, el mensaje de error creado por el código anterior sería "La longitud del nombre debe estar entre 6 y 8".

Para averiguar qué parámetros se pasan a String.Format para el mensaje de error de un atributo determinado, vea el código fuente de DataAnnotations.

Uso de los nombres de propiedad JSON en errores de validación

De forma predeterminada, cuando se produce un error de validación, la validación del modelo genera un ModelStateDictionary con el nombre de propiedad como la clave de error. Algunas aplicaciones, como las aplicaciones de una sola página, se benefician del uso de nombres de propiedad JSON para errores de validación generados a partir de las API web. El código siguiente configura la validación para usar SystemTextJsonValidationMetadataProvider con el fin de emplear nombres de propiedad JSON:

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

El código siguiente configura la validación para usar NewtonsoftJsonValidationMetadataProvider con el fin de emplear nombres de propiedad JSON al usar Json.NET:

using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Para obtener un ejemplo de la directiva para usar mayúsculas y minúsculas camel, consulte Program.cs en GitHub.

Tipos de referencia que no aceptan valores NULL y atributo [Required]

El sistema de validación trata las propiedades enlazadas o los parámetros que no aceptan valores NULL como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Al habilitar contextos Nullable, MVC inicia implícitamente la validación de propiedades o parámetros que no aceptan valores NULL como si se hubieran atribuido con el atributo [Required(AllowEmptyStrings = true)]. Observe el código siguiente:

public class Person
{
    public string Name { get; set; }
}

Si la aplicación se creó con <Nullable>enable</Nullable>, un valor que falta para Name en un formulario JSON genera un error de validación. Esto puede parecer contradictorio, ya que el atributo [Required(AllowEmptyStrings = true)] está implícito, pero se espera un comportamiento porque las cadenas vacías se convierten en null de forma predeterminada. Use un tipo de referencia que acepta valores NULL para permitir que se especifiquen valores NULL o que faltan para la propiedad Name:

public class Person
{
    public string? Name { get; set; }
}

Este comportamiento se puede deshabilitar si se configura SuppressImplicitRequiredAttributeForNonNullableReferenceTypes en Program.cs:

builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

Validación de [Required] en el servidor

En el servidor, si la propiedad es NULL, se considera que falta un valor requerido. Un campo que no acepta valores NULL siempre es válido y el mensaje de error del atributo [Required] no se muestra nunca.

Aun así, el enlace de modelos para una propiedad que no acepta valores NULL podría fallar, lo que genera un mensaje de error como The value '' is invalid. Para especificar un mensaje de error personalizado para la validación del lado servidor de tipos que no aceptan valores NULL, tiene las siguientes opciones:

  • Haga que el campo acepte valores NULL (por ejemplo, decimal? en lugar de decimal). Los tipos de valor Nullable<T> se tratan como tipos estándar que aceptan valores NULL.

  • Especifique el mensaje de error predeterminado que el enlace de modelos va a usar, como se muestra en el ejemplo siguiente:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

    Para obtener más información sobre los errores de enlace de modelos para los que se pueden establecer mensajes predeterminados, vea DefaultModelBindingMessageProvider.

Validación de [Required] en el cliente

Las cadenas y los tipos que no aceptan valores NULL se tratan de forma diferente en el cliente, en comparación con el servidor. En el cliente:

  • Un valor se considera presente solo si se especifica una entrada para él. Por lo tanto, la validación del lado cliente controla los tipos que no aceptan valores NULL del mismo modo que los tipos que aceptan valores NULL.
  • El método required de jQuery Validate considera que un espacio en blanco en un campo de cadena es una entrada válida. La validación del lado servidor considera que un campo de cadena necesario no es válido si solo se especifica un espacio en blanco.

Como se indicó anteriormente, los tipos que no aceptan valores NULL se tratan como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Esto significa que se obtiene la validación del lado cliente incluso si no se aplica el atributo [Required(AllowEmptyStrings = true)]. Si no usa el atributo, aparecerá un mensaje de error predeterminado. Para especificar un mensaje de error personalizado, use el atributo.

Atributo [Remote]

El atributo [Remote] implementa la validación del lado cliente que requiere llamar a un método en el servidor para determinar si la entrada del campo es válida. Por ejemplo, la aplicación podría tener que comprobar si un nombre de usuario ya está en uso.

Para implementar la validación remota:

  1. Cree un método de acción para que lo llame JavaScript. El método remote de Validación de jQuery espera una respuesta JSON:

    • true significa que los datos de entrada son válidos.
    • false, undefined o null significan que la entrada no es válida. Muestre el mensaje de error predeterminado.
    • Cualquier otra cadena significa que la entrada no es válida. Muestre la cadena como un mensaje de error personalizado.

    A continuación encontrará un ejemplo de un método de acción que devuelve un mensaje de error personalizado:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. En la clase de modelo, anote la propiedad con un atributo [Remote] que apunte al método de acción de validación, como se muestra en el ejemplo siguiente:

    [Remote(action: "VerifyEmail", controller: "Users")]
    public string Email { get; set; } = null!;
    

La validación del lado servidor también debe implementarse para los clientes que han deshabilitado JavaScript.

Campos adicionales

La propiedad AdditionalFields del atributo [Remote] permite validar combinaciones de campos con los datos del servidor. Por ejemplo, si el modelo User tuviera las propiedades FirstName y LastName, podría interesarle comprobar que ningún usuario actual tuviera ya ese par de nombres. En el siguiente ejemplo se muestra cómo usar AdditionalFields:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; } = null!;

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; } = null!;

AdditionalFields podría configurarse explícitamente para las cadenas "FirstName" y "LastName", pero, al usar el operador nameof, se simplifica la refactorización posterior. El método de acción para esta validación debe aceptar los argumentos firstName y lastName:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

Cuando el usuario escribe un nombre o un apellido, JavaScript realiza una llamada remota para comprobar si ese par de nombres ya existe.

Para validar dos o más campos adicionales, proporciónelos como una lista delimitada por comas. Por ejemplo, para agregar una propiedad MiddleName al modelo, establezca el atributo [Remote] tal como se muestra en el ejemplo siguiente:

[Remote(action: "VerifyName", controller: "Users",
    AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields, al igual que todos los argumentos de atributo, debe ser una expresión constante. Por lo tanto, no use una cadena interpolada ni llame a Join para inicializar AdditionalFields.

Alternativas a los atributos integrados

Si necesita una validación que no proporcionan los atributos integrados, puede hacer lo siguiente:

Atributos personalizados

Para los escenarios que no se controlan mediante los atributos de validación integrados, puede crear atributos de validación personalizados. Cree una clase que herede de ValidationAttribute y reemplace el método IsValid.

El método IsValid acepta un objeto denominado value, que es la entrada que se va a validar. Una sobrecarga también acepta un objeto ValidationContext, que proporciona información adicional, como la instancia del modelo creada por el enlace de modelos.

El siguiente ejemplo valida que la fecha de lanzamiento de una película del género Classic no sea posterior a un año especificado. El atributo [ClassicMovie]:

  • Solo se ejecuta en el servidor.
  • En el caso de las películas clásicas, valida la fecha de lanzamiento:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

La variable movie del ejemplo anterior representa un objeto Movie que contiene los datos del envío del formulario. Si se produce un error de validación, se devuelve un ValidationResult con un mensaje de error.

IValidatableObject

El ejemplo anterior solo funciona con tipos Movie. Otra opción para la validación del nivel de clase consiste en implementar IValidatableObject en la clase de modelo, como se muestra en el ejemplo siguiente:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}

Validación personalizada

El código siguiente muestra cómo agregar un error de modelo después de examinar el modelo:

if (Contact.Name == Contact.ShortName)
{
    ModelState.AddModelError("Contact.ShortName", 
                             "Short name can't be the same as Name.");
}

El código siguiente implementa la prueba de validación en un controlador:

if (contact.Name == contact.ShortName)
{
    ModelState.AddModelError(nameof(contact.ShortName),
                             "Short name can't be the same as Name.");
}

El código siguiente comprueba que el número de teléfono y el correo electrónico son únicos:

public async Task<IActionResult> OnPostAsync()
{
    // Attach Validation Error Message to the Model on validation failure.          

    if (Contact.Name == Contact.ShortName)
    {
        ModelState.AddModelError("Contact.ShortName", 
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == Contact.PhoneNumber))
    {
        ModelState.AddModelError("Contact.PhoneNumber",
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == Contact.Email))
    {
        ModelState.AddModelError("Contact.Email", "The Email is already in use.");
    }

    if (!ModelState.IsValid || _context.Contact == null || Contact == null)
    {
        // if model is invalid, return the page with the model state errors.
        return Page();
    }
    _context.Contact.Add(Contact);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

El código siguiente implementa la prueba de validación en un controlador:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Name,ShortName,Email,PhoneNumber")] Contact contact)
{
    // Attach Validation Error Message to the Model on validation failure.
    if (contact.Name == contact.ShortName)
    {
        ModelState.AddModelError(nameof(contact.ShortName),
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == contact.PhoneNumber))
    {
        ModelState.AddModelError(nameof(contact.PhoneNumber),
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == contact.Email))
    {
        ModelState.AddModelError(nameof(contact.Email), "The Email is already in use.");
    }

    if (ModelState.IsValid)
    {
        _context.Add(contact);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(contact);
}

La comprobación de un número de teléfono o correo electrónico únicos normalmente también se realiza con la validación remota.

ValidationResult

Fíjese en el siguiente ValidateNameAttribute personalizado:

public class ValidateNameAttribute : ValidationAttribute
{
    public ValidateNameAttribute()
    {
        const string defaultErrorMessage = "Error with Name";
        ErrorMessage ??= defaultErrorMessage;
    }

    protected override ValidationResult? IsValid(object? value,
                                         ValidationContext validationContext)
    {
        if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
        {
            return new ValidationResult("Name is required.");
        }

        if (value.ToString()!.ToLower().Contains("zz"))
        {

            return new ValidationResult(
                        FormatErrorMessage(validationContext.DisplayName));
        }

        return ValidationResult.Success;
    }
}

En el código siguiente, se aplica el atributo personalizado [ValidateName]:

public class Contact
{
    public Guid Id { get; set; }

    [ValidateName(ErrorMessage = "Name must not contain `zz`")] 
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
}

Cuando el modelo contiene zz, se devuelve un nuevo ValidationResult.

Validación de nodo de nivel superior

Los nodos de nivel superior incluyen lo siguiente:

  • Parámetros de acción
  • Propiedades de controlador
  • Parámetros de controlador de página
  • Propiedades de modelo de página

Los nodos de nivel superior enlazados al modelo se validan además de la validación de las propiedades del modelo. En el ejemplo siguiente de la aplicación de muestra, el método VerifyPhone usa RegularExpressionAttribute para validar el parámetro de acción phone:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

Los nodos de nivel superior pueden usar BindRequiredAttribute con atributos de validación. En el ejemplo siguiente de la aplicación de muestra, el método CheckAge especifica que el parámetro age debe estar enlazado desde la cadena de consulta al enviar el formulario:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

En la página Comprobar edad (CheckAge.cshtml), hay dos formularios. El primer formulario envía un valor Age de 99 como una cadena de parámetro de consulta: https://localhost:5001/Users/CheckAge?Age=99.

Al enviar un parámetro age con un formato correcto desde la cadena de consulta, el formulario se valida.

El segundo formulario de la página Comprobar edad envía el valor Age en el cuerpo de la solicitud, y se produce un error de validación. Se produce un error en el enlace porque el parámetro age debe provenir de una cadena de consulta.

Número máximo de errores

La validación se detiene cuando se alcanza el número máximo de errores (200 de forma predeterminada). Puede configurar este número con el siguiente código en Program.cs:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

Recursividad máxima

ValidationVisitor recorre el gráfico de objetos del modelo que se está validando. En el caso de los modelos profundos o infinitamente recursivos, la validación podría causar un desbordamiento de pila. MvcOptions.MaxValidationDepth proporciona una manera de detener pronto la validación si la recursividad del visitante supera la profundidad configurada. El valor predeterminado de MvcOptions.MaxValidationDepth es 32.

Cortocircuito automático

La validación cortocircuita (se omite) automáticamente si el gráfico de modelo no requiere validación. Entre los objetos para los que el tiempo de ejecución omite la validación se incluyen las colecciones de elementos primitivos (como byte[], string[] y Dictionary<string, string>) y gráficos de objeto complejo que no tienen los validadores.

Validación del lado cliente

La validación del lado cliente impide realizar el envío hasta que el formulario sea válido. El botón Enviar ejecuta JavaScript para enviar el formulario o mostrar mensajes de error.

La validación del lado cliente evita un recorrido de ida y vuelta innecesario en el servidor cuando hay errores de entrada en un formulario. Las referencias de script siguientes en _Layout.cshtml y _ValidationScriptsPartial.cshtml admiten la validación del lado del cliente:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>

El script Validación discreta de jQuery es una biblioteca front-end personalizada de Microsoft que se basa en el conocido complemento Validación de jQuery. Si no usa Validación discreta de jQuery, deberá codificar la misma lógica de validación dos veces: una vez en los atributos de validación del lado servidor en las propiedades del modelo y luego en los scripts del lado cliente. En su lugar, los asistentes de etiquetas y los asistentes de HTML usan los atributos de validación y escriben metadatos de las propiedades del modelo para representar atributos data- HTML 5 para los elementos de formulario que necesitan validación. Validación discreta de jQuery analiza los atributos data- y pasa la lógica a la Validación de jQuery. De este modo, la lógica de validación del lado servidor se "copia" de manera eficaz en el cliente. Puede mostrar errores de validación en el cliente mediante el uso de asistentes de etiquetas, como se muestra aquí:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

Los anteriores asistentes de etiquetas representan el siguiente código HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

Tenga en cuenta que los atributos data- en los resultados HTML corresponden a los atributos de validación para la propiedad Movie.ReleaseDate. El atributo data-val-required contiene un mensaje de error que se muestra si el usuario no rellena el campo de fecha de estreno. La validación discreta de jQuery pasa este valor al método required() de la validación de jQuery, que luego muestra ese mensaje en el elemento <span> que lo acompaña.

La validación del tipo de datos se basa en el tipo .NET de una propiedad, a menos que lo reemplace un atributo [DataType]. Los exploradores tienen sus propios mensajes de error de predeterminados, pero el paquete de Validación discreta de jQuery Validate puede invalidar esos mensajes. Los atributos y las subclases [DataType], como [EmailAddress], permiten especificar el mensaje de error.

Validación discreta

Para obtener información sobre la validación discreta, consulte este problema de GitHub.

Agregar validación a formularios dinámicos

Validación discreta de jQuery pasa los parámetros y la lógica de validación a la Validación de jQuery cuando la página se carga por primera vez. Por lo tanto, la validación no funciona automáticamente en los formularios generados dinámicamente. Para habilitar la validación, hay que indicarle a Validación discreta de jQuery que analice el formulario dinámico inmediatamente después de su creación. Por ejemplo, en el código siguiente se configura la validación del lado cliente en un formulario agregado mediante AJAX.

$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})

El método $.validator.unobtrusive.parse() acepta un selector de jQuery para su único argumento. Este método indica a Validación discreta de jQuery que analice los atributos data- de formularios dentro de ese selector. Después, los valores de estos atributos se pasan al complemento de validación de jQuery.

Agregar validación a controles dinámicos

El método $.validator.unobtrusive.parse() funciona en todo el formulario, no en los controles individuales generados dinámicamente, como <input> y <select/>. Para volver a analizar el formulario, quite los datos de validación que se agregaron cuando el formulario se analizó anteriormente, como se muestra en el ejemplo siguiente:

$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

Validación del lado cliente personalizada

Para personalizar la validación del lado cliente, es necesario generar atributos HTML data- que funcionen con un adaptador personalizado de validación de jQuery. El siguiente ejemplo de código de adaptador se escribió para los atributos [ClassicMovie] y [ClassicMovieWithClientValidator] que se introdujeron anteriormente en este artículo:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

Para obtener información sobre cómo escribir adaptadores, vea la documentación de validación de jQuery.

El uso de un adaptador para un campo determinado se desencadena mediante atributos data- que:

  • Marcan que el campo está sujeto a validación (data-val="true").
  • Identifican un nombre de regla de validación y un texto de mensaje de error (por ejemplo, data-val-rulename="Error message.").
  • Proporcionan los parámetros adicionales que necesite el validador (por ejemplo, data-val-rulename-param1="value").

En el ejemplo siguiente se muestran los atributos data- para el atributo ClassicMovie de la aplicación de ejemplo:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

Como se indicó anteriormente, los asistentes de etiquetas y los asistentes de HTML usan información procedente de los atributos de validación para representar atributos data-. Hay dos opciones para escribir código que dé como resultado la creación de atributos HTML data- personalizados:

  • Puede crear una clase que derive de AttributeAdapterBase<TAttribute> y una clase que implemente IValidationAttributeAdapterProvider y, después, registrar el atributo y su adaptador en la inserción de dependencias. Este método sigue el principio de responsabilidad única, ya que el código de validación relacionado con servidor y el relacionado con el cliente se encuentran en clases independientes. El adaptador también cuenta con una ventaja: debido a que está registrado en la inserción de dependencias, tiene a su disposición otros servicios de la inserción de dependencias si es necesario.
  • Puede implementar IClientModelValidator en la clase ValidationAttribute. Este método es adecuado si el atributo no realiza ninguna validación en el lado servidor y no necesita ningún servicio de la inserción de dependencias.

AttributeAdapter para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovie en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  1. Cree una clase de adaptador de atributo para el atributo de validación personalizado. Derive la clase de AttributeAdapterBase<TAttribute>. Cree un método AddValidation que agregue atributos data- a la salida representada, tal como se muestra en este ejemplo:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(
            ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
            => Attribute.GetErrorMessage();
    }
    
  2. Cree una clase de proveedor de adaptador que implemente IValidationAttributeAdapterProvider. En el método GetAttributeAdapter, pase el atributo personalizado al constructor del adaptador, como se muestra en este ejemplo:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter? GetAttributeAdapter(
            ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Registre el proveedor del adaptador para la inserción de dependencias en Program.cs:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

IClientModelValidator para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovieWithClientValidator en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  • En el atributo de validación personalizado, implemente la interfaz IClientModelValidator y cree un método AddValidation. En el método AddValidation, agregue atributos data- para la validación, como se muestra en el ejemplo siguiente:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
            => Year = year;
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());
    
            var year = Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult? IsValid(
            object? value, ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value!).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

Deshabilitación de la validación del lado cliente

El código siguiente deshabilita la validación de cliente en las Razor Pages:

builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

Otras opciones para deshabilitar la validación del lado cliente:

  • Convierta en comentario la referencia a _ValidationScriptsPartial en todos los archivos .cshtml.
  • Quite el contenido del archivo Pages\Shared_ValidationScriptsPartial.cshtml.

El enfoque anterior no impedirá la validación del lado cliente de la biblioteca de clases de IdentityRazor de ASP.NET Core. Para obtener más información, vea Scaffolding Identity en proyectos de ASP.NET Core.

Detalles del problema

Los detalles del problema no son el único formato de respuesta para describir un error de la API HTTP; sin embargo, se usan normalmente para notificar errores para las API HTTP.

El servicio de detalles del problema implementa la interfaz IProblemDetailsService, que admite la creación de detalles del problema en ASP.NET Core. El método de extensión AddProblemDetails de IServiceCollection registra la implementación IProblemDetailsService predeterminada.

En aplicaciones de ASP.NET Core, el middleware siguiente genera respuestas HTTP de detalles del problema cuando se llama a AddProblemDetails, excepto cuando el encabezado HTTP de solicitud Accept no incluye uno de los tipos de contenido admitidos por el IProblemDetailsWriter registrado (valor predeterminado: application/json):

Recursos adicionales

En este artículo se explica cómo validar la entrada del usuario en una aplicación ASP.NET Core MVC o Razor Pages.

Vea o descargue el código de ejemplo (cómo descargarlo).

Estado del modelo

El estado del modelo representa los errores que proceden de dos subsistemas: el enlace de modelos y la validación de modelos. Los errores que se originan del enlace de modelos suelen ser errores de conversión de datos. Por ejemplo, se escribe una "x" en un campo numérico entero. La validación del modelo se produce después del enlace de modelos y notifica los errores en los que los datos no cumplen las reglas de negocio. Por ejemplo, se especifica un 0 en un campo que espera una clasificación entre 1 y 5.

Tanto el enlace como la validación de modelos se producen antes de la ejecución de una acción de controlador o un método de controlador de Razor Pages. En el caso de las aplicaciones web, la aplicación es responsable de inspeccionar ModelState.IsValid y reaccionar de manera apropiada. Normalmente, las aplicaciones web vuelven a reproducir la página con un mensaje de error, como se muestra en el ejemplo de Razor Pages siguiente:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Para ASP.NET Core MVC con controladores y vistas, en el ejemplo siguiente se muestra cómo revisar ModelState.IsValid dentro de una acción de controlador:

public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}

Los controladores de Web API no tienen que comprobar si ModelState.IsValid tienen el atributo [ApiController]. En ese caso, se devuelve una respuesta HTTP 400 automática que contiene los detalles del error cuando el estado del modelo no es válido. Para obtener más información, consulte Respuestas HTTP 400 automáticas.

Nueva ejecución de la validación

La validación es automática, pero tal vez le interese repetirla manualmente. Por ejemplo, tal vez haya calculado el valor de una propiedad y quiera volver a ejecutar la validación después de establecer la propiedad en el valor calculado. Para volver a ejecutar la validación, llame a ModelStateDictionary.ClearValidationState para borrar la validación específica del modelo que se está validando seguida de TryValidateModel:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Atributos de validación

Los atributos de validación permiten especificar reglas de validación para las propiedades del modelo. En el ejemplo siguiente de la aplicación de ejemplo se muestra una clase de modelo anotada con atributos de validación. El atributo [ClassicMovie] es un atributo de validación personalizado y los demás están integrados. No se muestra [ClassicMovieWithClientValidator], que indica una manera alternativa de implementar un atributo personalizado.

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

Atributos integrados

Estos son algunos de los atributos de validación integrados:

  • [ValidateNever]: indica que una propiedad o parámetro se debe excluir de la validación.
  • [CreditCard]: valida que la propiedad tenga formato de tarjeta de crédito. Requiere métodos adicionales de validación de jQuery.
  • [Compare]: valida que dos propiedades de un modelo coincidan.
  • [EmailAddress]: valida que la propiedad tenga formato de correo electrónico.
  • [Phone]: valida que la propiedad tenga formato de número de teléfono.
  • [Range]: valida que el valor de propiedad se encuentre dentro de un intervalo especificado.
  • [RegularExpression]: valida que el valor de propiedad coincida con una expresión regular especificada.
  • [Required]: valida que el campo no es NULL. Consulte el atributo [Required] para obtener más información sobre el comportamiento de este atributo.
  • [StringLength]: valida que un valor de propiedad de cadena no supere un límite de longitud especificado.
  • [Url]: valida que la propiedad tenga un formato de URL.
  • [Remote]: valida la entrada en el cliente mediante una llamada a un método de acción en el servidor. Consulte el atributo [Remote] para obtener más información sobre el comportamiento de este atributo.

En el espacio de nombres System.ComponentModel.DataAnnotations encontrará una lista completa de atributos de validación.

Mensajes de error

Los atributos de validación permiten especificar el mensaje de error que se mostrará para una entrada no válida. Por ejemplo:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

Internamente, los atributos llaman a String.Format con un marcador de posición para el nombre de campo y, en ocasiones, marcadores de posición adicionales. Por ejemplo:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

Cuando se aplica a una propiedad Name, el mensaje de error creado por el código anterior sería "La longitud del nombre debe estar entre 6 y 8".

Para averiguar qué parámetros se pasan a String.Format para el mensaje de error de un atributo determinado, vea el código fuente de DataAnnotations.

Tipos de referencia que no aceptan valores NULL y atributo [Required]

El sistema de validación trata las propiedades enlazadas o los parámetros que no aceptan valores NULL como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Al habilitar contextosNullable, MVC inicia implícitamente la validación de propiedades o parámetros que no aceptan valores NULL en tipos no genéricos como si se hubieran atribuido con el atributo [Required(AllowEmptyStrings = true)]. Observe el código siguiente:

public class Person
{
    public string Name { get; set; }
}

Si la aplicación se creó con <Nullable>enable</Nullable>, un valor que falta para Name en un formulario JSON genera un error de validación. Use un tipo de referencia que acepta valores NULL para permitir que se especifiquen valores NULL o que faltan para la propiedad Name:

public class Person
{
    public string? Name { get; set; }
}

Este comportamiento se puede deshabilitar si se configura SuppressImplicitRequiredAttributeForNonNullableReferenceTypes en Program.cs:

builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

Propiedades que no aceptan valores NULL en tipos genéricos y atributo [Required]

Las propiedades que no aceptan valores NULL en tipos genéricos deben incluir el atributo [Required] cuando se requiere el tipo. En el código siguiente, TestRequired no es necesario:

public class WeatherForecast<T>
{
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}

En el código siguiente, TestRequired se marca explícitamente como obligatorio:

using System.ComponentModel.DataAnnotations;

public class WeatherForecast<T>
{
    [Required]
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}

Validación de [Required] en el servidor

En el servidor, si la propiedad es NULL, se considera que falta un valor requerido. Un campo que no acepta valores NULL siempre es válido y el mensaje de error del atributo [Required] no se muestra nunca.

Aun así, el enlace de modelos para una propiedad que no acepta valores NULL podría fallar, lo que genera un mensaje de error como The value '' is invalid. Para especificar un mensaje de error personalizado para la validación del lado servidor de tipos que no aceptan valores NULL, tiene las siguientes opciones:

  • Haga que el campo acepte valores NULL (por ejemplo, decimal? en lugar de decimal). Los tipos de valor Nullable<T> se tratan como tipos estándar que aceptan valores NULL.

  • Especifique el mensaje de error predeterminado que el enlace de modelos va a usar, como se muestra en el ejemplo siguiente:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

    Para obtener más información sobre los errores de enlace de modelos para los que se pueden establecer mensajes predeterminados, vea DefaultModelBindingMessageProvider.

Validación de [Required] en el cliente

Las cadenas y los tipos que no aceptan valores NULL se tratan de forma diferente en el cliente, en comparación con el servidor. En el cliente:

  • Un valor se considera presente solo si se especifica una entrada para él. Por lo tanto, la validación del lado cliente controla los tipos que no aceptan valores NULL del mismo modo que los tipos que aceptan valores NULL.
  • El método required de jQuery Validate considera que un espacio en blanco en un campo de cadena es una entrada válida. La validación del lado servidor considera que un campo de cadena necesario no es válido si solo se especifica un espacio en blanco.

Como se indicó anteriormente, los tipos que no aceptan valores NULL se tratan como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Esto significa que se obtiene la validación del lado cliente incluso si no se aplica el atributo [Required(AllowEmptyStrings = true)]. Si no usa el atributo, aparecerá un mensaje de error predeterminado. Para especificar un mensaje de error personalizado, use el atributo.

Atributo [Remote]

El atributo [Remote] implementa la validación del lado cliente que requiere llamar a un método en el servidor para determinar si la entrada del campo es válida. Por ejemplo, la aplicación podría tener que comprobar si un nombre de usuario ya está en uso.

Para implementar la validación remota:

  1. Cree un método de acción para que lo llame JavaScript. El método remote de Validación de jQuery espera una respuesta JSON:

    • true significa que los datos de entrada son válidos.
    • false, undefined o null significan que la entrada no es válida. Muestre el mensaje de error predeterminado.
    • Cualquier otra cadena significa que la entrada no es válida. Muestre la cadena como un mensaje de error personalizado.

    A continuación encontrará un ejemplo de un método de acción que devuelve un mensaje de error personalizado:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. En la clase de modelo, anote la propiedad con un atributo [Remote] que apunte al método de acción de validación, como se muestra en el ejemplo siguiente:

    [Remote(action: "VerifyEmail", controller: "Users")]
    public string Email { get; set; } = null!;
    

Campos adicionales

La propiedad AdditionalFields del atributo [Remote] permite validar combinaciones de campos con los datos del servidor. Por ejemplo, si el modelo User tuviera las propiedades FirstName y LastName, podría interesarle comprobar que ningún usuario actual tuviera ya ese par de nombres. En el siguiente ejemplo se muestra cómo usar AdditionalFields:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; } = null!;

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; } = null!;

AdditionalFields podría configurarse explícitamente para las cadenas "FirstName" y "LastName", pero, al usar el operador nameof, se simplifica la refactorización posterior. El método de acción para esta validación debe aceptar los argumentos firstName y lastName:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

Cuando el usuario escribe un nombre o un apellido, JavaScript realiza una llamada remota para comprobar si ese par de nombres ya existe.

Para validar dos o más campos adicionales, proporciónelos como una lista delimitada por comas. Por ejemplo, para agregar una propiedad MiddleName al modelo, establezca el atributo [Remote] tal como se muestra en el ejemplo siguiente:

[Remote(action: "VerifyName", controller: "Users",
    AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields, al igual que todos los argumentos de atributo, debe ser una expresión constante. Por lo tanto, no use una cadena interpolada ni llame a Join para inicializar AdditionalFields.

Alternativas a los atributos integrados

Si necesita una validación que no proporcionan los atributos integrados, puede hacer lo siguiente:

Atributos personalizados

Para los escenarios que no se controlan mediante los atributos de validación integrados, puede crear atributos de validación personalizados. Cree una clase que herede de ValidationAttribute y reemplace el método IsValid.

El método IsValid acepta un objeto denominado value, que es la entrada que se va a validar. Una sobrecarga también acepta un objeto ValidationContext, que proporciona información adicional, como la instancia del modelo creada por el enlace de modelos.

El siguiente ejemplo valida que la fecha de lanzamiento de una película del género Classic no sea posterior a un año especificado. El atributo [ClassicMovie]:

  • Solo se ejecuta en el servidor.
  • En el caso de las películas clásicas, valida la fecha de lanzamiento:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

La variable movie del ejemplo anterior representa un objeto Movie que contiene los datos del envío del formulario. Si se produce un error de validación, se devuelve un ValidationResult con un mensaje de error.

IValidatableObject

El ejemplo anterior solo funciona con tipos Movie. Otra opción para la validación del nivel de clase consiste en implementar IValidatableObject en la clase de modelo, como se muestra en el ejemplo siguiente:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}

Validación de nodo de nivel superior

Los nodos de nivel superior incluyen lo siguiente:

  • Parámetros de acción
  • Propiedades de controlador
  • Parámetros de controlador de página
  • Propiedades de modelo de página

Los nodos de nivel superior enlazados al modelo se validan además de la validación de las propiedades del modelo. En el ejemplo siguiente de la aplicación de muestra, el método VerifyPhone usa RegularExpressionAttribute para validar el parámetro de acción phone:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

Los nodos de nivel superior pueden usar BindRequiredAttribute con atributos de validación. En el ejemplo siguiente de la aplicación de muestra, el método CheckAge especifica que el parámetro age debe estar enlazado desde la cadena de consulta al enviar el formulario:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

En la página Comprobar edad (CheckAge.cshtml), hay dos formularios. El primer formulario envía un valor Age de 99 como una cadena de parámetro de consulta: https://localhost:5001/Users/CheckAge?Age=99.

Al enviar un parámetro age con un formato correcto desde la cadena de consulta, el formulario se valida.

El segundo formulario de la página Comprobar edad envía el valor Age en el cuerpo de la solicitud, y se produce un error de validación. Se produce un error en el enlace porque el parámetro age debe provenir de una cadena de consulta.

Número máximo de errores

La validación se detiene cuando se alcanza el número máximo de errores (200 de forma predeterminada). Puede configurar este número con el siguiente código en Program.cs:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

Recursividad máxima

ValidationVisitor recorre el gráfico de objetos del modelo que se está validando. En el caso de los modelos profundos o infinitamente recursivos, la validación podría causar un desbordamiento de pila. MvcOptions.MaxValidationDepth proporciona una manera de detener pronto la validación si la recursividad del visitante supera la profundidad configurada. El valor predeterminado de MvcOptions.MaxValidationDepth es 32.

Cortocircuito automático

La validación cortocircuita (se omite) automáticamente si el gráfico de modelo no requiere validación. Entre los objetos para los que el tiempo de ejecución omite la validación se incluyen las colecciones de elementos primitivos (como byte[], string[] y Dictionary<string, string>) y gráficos de objeto complejo que no tienen los validadores.

Validación del lado cliente

La validación del lado cliente impide realizar el envío hasta que el formulario sea válido. El botón Enviar ejecuta JavaScript para enviar el formulario o mostrar mensajes de error.

La validación del lado cliente evita un recorrido de ida y vuelta innecesario en el servidor cuando hay errores de entrada en un formulario. Las referencias de script siguientes en _Layout.cshtml y _ValidationScriptsPartial.cshtml admiten la validación del lado del cliente:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>

El script Validación discreta de jQuery es una biblioteca front-end personalizada de Microsoft que se basa en el conocido complemento Validación de jQuery. Si no usa Validación discreta de jQuery, deberá codificar la misma lógica de validación dos veces: una vez en los atributos de validación del lado servidor en las propiedades del modelo y luego en los scripts del lado cliente. En su lugar, los asistentes de etiquetas y los asistentes de HTML usan los atributos de validación y escriben metadatos de las propiedades del modelo para representar atributos data- HTML 5 para los elementos de formulario que necesitan validación. Validación discreta de jQuery analiza los atributos data- y pasa la lógica a la Validación de jQuery. De este modo, la lógica de validación del lado servidor se "copia" de manera eficaz en el cliente. Puede mostrar errores de validación en el cliente mediante el uso de asistentes de etiquetas, como se muestra aquí:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

Los anteriores asistentes de etiquetas representan el siguiente código HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

Tenga en cuenta que los atributos data- en los resultados HTML corresponden a los atributos de validación para la propiedad Movie.ReleaseDate. El atributo data-val-required contiene un mensaje de error que se muestra si el usuario no rellena el campo de fecha de estreno. La validación discreta de jQuery pasa este valor al método required() de la validación de jQuery, que luego muestra ese mensaje en el elemento <span> que lo acompaña.

La validación del tipo de datos se basa en el tipo .NET de una propiedad, a menos que lo reemplace un atributo [DataType]. Los exploradores tienen sus propios mensajes de error de predeterminados, pero el paquete de Validación discreta de jQuery Validate puede invalidar esos mensajes. Los atributos y las subclases [DataType], como [EmailAddress], permiten especificar el mensaje de error.

Validación discreta

Para obtener información sobre la validación discreta, consulte este problema de GitHub.

Agregar validación a formularios dinámicos

Validación discreta de jQuery pasa los parámetros y la lógica de validación a la Validación de jQuery cuando la página se carga por primera vez. Por lo tanto, la validación no funciona automáticamente en los formularios generados dinámicamente. Para habilitar la validación, hay que indicarle a Validación discreta de jQuery que analice el formulario dinámico inmediatamente después de su creación. Por ejemplo, en el código siguiente se configura la validación del lado cliente en un formulario agregado mediante AJAX.

$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})

El método $.validator.unobtrusive.parse() acepta un selector de jQuery para su único argumento. Este método indica a Validación discreta de jQuery que analice los atributos data- de formularios dentro de ese selector. Después, los valores de estos atributos se pasan al complemento de validación de jQuery.

Agregar validación a controles dinámicos

El método $.validator.unobtrusive.parse() funciona en todo el formulario, no en los controles individuales generados dinámicamente, como <input> y <select/>. Para volver a analizar el formulario, quite los datos de validación que se agregaron cuando el formulario se analizó anteriormente, como se muestra en el ejemplo siguiente:

$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

Validación del lado cliente personalizada

Para personalizar la validación del lado cliente, es necesario generar atributos HTML data- que funcionen con un adaptador personalizado de validación de jQuery. El siguiente ejemplo de código de adaptador se escribió para los atributos [ClassicMovie] y [ClassicMovieWithClientValidator] que se introdujeron anteriormente en este artículo:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

Para obtener información sobre cómo escribir adaptadores, vea la documentación de validación de jQuery.

El uso de un adaptador para un campo determinado se desencadena mediante atributos data- que:

  • Marcan que el campo está sujeto a validación (data-val="true").
  • Identifican un nombre de regla de validación y un texto de mensaje de error (por ejemplo, data-val-rulename="Error message.").
  • Proporcionan los parámetros adicionales que necesite el validador (por ejemplo, data-val-rulename-param1="value").

En el ejemplo siguiente se muestran los atributos data- para el atributo ClassicMovie de la aplicación de ejemplo:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

Como se indicó anteriormente, los asistentes de etiquetas y los asistentes de HTML usan información procedente de los atributos de validación para representar atributos data-. Hay dos opciones para escribir código que dé como resultado la creación de atributos HTML data- personalizados:

  • Puede crear una clase que derive de AttributeAdapterBase<TAttribute> y una clase que implemente IValidationAttributeAdapterProvider y, después, registrar el atributo y su adaptador en la inserción de dependencias. Este método sigue el principio de responsabilidad única, ya que el código de validación relacionado con servidor y el relacionado con el cliente se encuentran en clases independientes. El adaptador también cuenta con una ventaja: debido a que está registrado en la inserción de dependencias, tiene a su disposición otros servicios de la inserción de dependencias si es necesario.
  • Puede implementar IClientModelValidator en la clase ValidationAttribute. Este método es adecuado si el atributo no realiza ninguna validación en el lado servidor y no necesita ningún servicio de la inserción de dependencias.

AttributeAdapter para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovie en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  1. Cree una clase de adaptador de atributo para el atributo de validación personalizado. Derive la clase de AttributeAdapterBase<TAttribute>. Cree un método AddValidation que agregue atributos data- a la salida representada, tal como se muestra en este ejemplo:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(
            ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
            => Attribute.GetErrorMessage();
    }
    
  2. Cree una clase de proveedor de adaptador que implemente IValidationAttributeAdapterProvider. En el método GetAttributeAdapter, pase el atributo personalizado al constructor del adaptador, como se muestra en este ejemplo:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter? GetAttributeAdapter(
            ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Registre el proveedor del adaptador para la inserción de dependencias en Program.cs:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

IClientModelValidator para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovieWithClientValidator en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  • En el atributo de validación personalizado, implemente la interfaz IClientModelValidator y cree un método AddValidation. En el método AddValidation, agregue atributos data- para la validación, como se muestra en el ejemplo siguiente:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
            => Year = year;
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());
    
            var year = Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult? IsValid(
            object? value, ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value!).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

Deshabilitación de la validación del lado cliente

El código siguiente deshabilita la validación de cliente en las Razor Pages:

builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

Otras opciones para deshabilitar la validación del lado cliente:

  • Convierta en comentario la referencia a _ValidationScriptsPartial en todos los archivos .cshtml.
  • Quite el contenido del archivo Pages\Shared_ValidationScriptsPartial.cshtml.

El enfoque anterior no impedirá la validación del lado cliente de la biblioteca de clases de IdentityRazor de ASP.NET Core. Para obtener más información, vea Scaffolding Identity en proyectos de ASP.NET Core.

Recursos adicionales

En este artículo se explica cómo validar la entrada del usuario en una aplicación ASP.NET Core MVC o Razor Pages.

Vea o descargue el código de ejemplo (cómo descargarlo).

Estado del modelo

El estado del modelo representa los errores que proceden de dos subsistemas: el enlace de modelos y la validación de modelos. Los errores que se originan del enlace de modelos suelen ser errores de conversión de datos. Por ejemplo, se escribe una "x" en un campo numérico entero. La validación del modelo se produce después del enlace de modelos y notifica los errores en los que los datos no cumplen las reglas de negocio. Por ejemplo, se especifica un 0 en un campo que espera una clasificación entre 1 y 5.

Tanto el enlace como la validación de modelos se producen antes de la ejecución de una acción de controlador o un método de controlador de Razor Pages. En el caso de las aplicaciones web, la aplicación es responsable de inspeccionar ModelState.IsValid y reaccionar de manera apropiada. Normalmente, las aplicaciones web vuelven a mostrar la página con un mensaje de error:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Los controladores de Web API no tienen que comprobar si ModelState.IsValid tienen el atributo [ApiController]. En ese caso, se devuelve una respuesta HTTP 400 automática que contiene los detalles del error cuando el estado del modelo no es válido. Para obtener más información, consulte Respuestas HTTP 400 automáticas.

Nueva ejecución de la validación

La validación es automática, pero tal vez le interese repetirla manualmente. Por ejemplo, tal vez haya calculado el valor de una propiedad y quiera volver a ejecutar la validación después de establecer la propiedad en el valor calculado. Para volver a ejecutar la validación, llame a ModelStateDictionary.ClearValidationState para borrar la validación específica del modelo que se está validando seguida de TryValidateModel:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Atributos de validación

Los atributos de validación permiten especificar reglas de validación para las propiedades del modelo. En el ejemplo siguiente de la aplicación de ejemplo se muestra una clase de modelo anotada con atributos de validación. El atributo [ClassicMovie] es un atributo de validación personalizado y los demás están integrados. No se muestra [ClassicMovieWithClientValidator], que indica una manera alternativa de implementar un atributo personalizado.

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

Atributos integrados

Estos son algunos de los atributos de validación integrados:

  • [ValidateNever]: indica que una propiedad o parámetro se debe excluir de la validación.
  • [CreditCard]: valida que la propiedad tenga formato de tarjeta de crédito. Requiere métodos adicionales de validación de jQuery.
  • [Compare]: valida que dos propiedades de un modelo coincidan.
  • [EmailAddress]: valida que la propiedad tenga formato de correo electrónico.
  • [Phone]: valida que la propiedad tenga formato de número de teléfono.
  • [Range]: valida que el valor de propiedad se encuentre dentro de un intervalo especificado.
  • [RegularExpression]: valida que el valor de propiedad coincida con una expresión regular especificada.
  • [Required]: valida que el campo no es NULL. Consulte el atributo [Required] para obtener más información sobre el comportamiento de este atributo.
  • [StringLength]: valida que un valor de propiedad de cadena no supere un límite de longitud especificado.
  • [Url]: valida que la propiedad tenga un formato de URL.
  • [Remote]: valida la entrada en el cliente mediante una llamada a un método de acción en el servidor. Consulte el atributo [Remote] para obtener más información sobre el comportamiento de este atributo.

En el espacio de nombres System.ComponentModel.DataAnnotations encontrará una lista completa de atributos de validación.

Mensajes de error

Los atributos de validación permiten especificar el mensaje de error que se mostrará para una entrada no válida. Por ejemplo:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

Internamente, los atributos llaman a String.Format con un marcador de posición para el nombre de campo y, en ocasiones, marcadores de posición adicionales. Por ejemplo:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

Cuando se aplica a una propiedad Name, el mensaje de error creado por el código anterior sería "La longitud del nombre debe estar entre 6 y 8".

Para averiguar qué parámetros se pasan a String.Format para el mensaje de error de un atributo determinado, vea el código fuente de DataAnnotations.

Tipos de referencia que no aceptan valores NULL y atributo [Required]

El sistema de validación trata las propiedades enlazadas o los parámetros que no aceptan valores NULL como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Al habilitar contextos Nullable, MVC inicia implícitamente la validación de propiedades o parámetros que no aceptan valores NULL como si se hubieran atribuido con el atributo [Required(AllowEmptyStrings = true)]. Observe el código siguiente:

public class Person
{
    public string Name { get; set; }
}

Si la aplicación se creó con <Nullable>enable</Nullable>, un valor que falta para Name en un formulario JSON genera un error de validación. Use un tipo de referencia que acepta valores NULL para permitir que se especifiquen valores NULL o que faltan para la propiedad Name:

public class Person
{
    public string? Name { get; set; }
}

Este comportamiento se puede deshabilitar si se configura SuppressImplicitRequiredAttributeForNonNullableReferenceTypes en Startup.ConfigureServices:

services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

Validación de [Required] en el servidor

En el servidor, si la propiedad es NULL, se considera que falta un valor requerido. Un campo que no acepta valores NULL siempre es válido y el mensaje de error del atributo [Required] no se muestra nunca.

Aun así, el enlace de modelos para una propiedad que no acepta valores NULL podría fallar, lo que genera un mensaje de error como The value '' is invalid. Para especificar un mensaje de error personalizado para la validación del lado servidor de tipos que no aceptan valores NULL, tiene las siguientes opciones:

  • Haga que el campo acepte valores NULL (por ejemplo, decimal? en lugar de decimal). Los tipos de valor Nullable<T> se tratan como tipos estándar que aceptan valores NULL.

  • Especifique el mensaje de error predeterminado que el enlace de modelos va a usar, como se muestra en el ejemplo siguiente:

    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    services.AddSingleton<IValidationAttributeAdapterProvider,
        CustomValidationAttributeAdapterProvider>();
    

    Para obtener más información sobre los errores de enlace de modelos para los que se pueden establecer mensajes predeterminados, vea DefaultModelBindingMessageProvider.

Validación de [Required] en el cliente

Las cadenas y los tipos que no aceptan valores NULL se tratan de forma diferente en el cliente, en comparación con el servidor. En el cliente:

  • Un valor se considera presente solo si se especifica una entrada para él. Por lo tanto, la validación del lado cliente controla los tipos que no aceptan valores NULL del mismo modo que los tipos que aceptan valores NULL.
  • El método required de jQuery Validate considera que un espacio en blanco en un campo de cadena es una entrada válida. La validación del lado servidor considera que un campo de cadena necesario no es válido si solo se especifica un espacio en blanco.

Como se indicó anteriormente, los tipos que no aceptan valores NULL se tratan como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Esto significa que se obtiene la validación del lado cliente incluso si no se aplica el atributo [Required(AllowEmptyStrings = true)]. Si no usa el atributo, aparecerá un mensaje de error predeterminado. Para especificar un mensaje de error personalizado, use el atributo.

Atributo [Remote]

El atributo [Remote] implementa la validación del lado cliente que requiere llamar a un método en el servidor para determinar si la entrada del campo es válida. Por ejemplo, la aplicación podría tener que comprobar si un nombre de usuario ya está en uso.

Para implementar la validación remota:

  1. Cree un método de acción para que lo llame JavaScript. El método remote de Validación de jQuery espera una respuesta JSON:

    • true significa que los datos de entrada son válidos.
    • false, undefined o null significan que la entrada no es válida. Muestre el mensaje de error predeterminado.
    • Cualquier otra cadena significa que la entrada no es válida. Muestre la cadena como un mensaje de error personalizado.

    A continuación encontrará un ejemplo de un método de acción que devuelve un mensaje de error personalizado:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. En la clase de modelo, anote la propiedad con un atributo [Remote] que apunte al método de acción de validación, como se muestra en el ejemplo siguiente:

    [Remote(action: "VerifyEmail", controller: "Users")]
    public string Email { get; set; }
    

Campos adicionales

La propiedad AdditionalFields del atributo [Remote] permite validar combinaciones de campos con los datos del servidor. Por ejemplo, si el modelo User tuviera las propiedades FirstName y LastName, podría interesarle comprobar que ningún usuario actual tuviera ya ese par de nombres. En el siguiente ejemplo se muestra cómo usar AdditionalFields:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; }

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; }

AdditionalFields podría configurarse explícitamente para las cadenas "FirstName" y "LastName", pero, al usar el operador nameof, se simplifica la refactorización posterior. El método de acción para esta validación debe aceptar los argumentos firstName y lastName:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

Cuando el usuario escribe un nombre o un apellido, JavaScript realiza una llamada remota para comprobar si ese par de nombres ya existe.

Para validar dos o más campos adicionales, proporciónelos como una lista delimitada por comas. Por ejemplo, para agregar una propiedad MiddleName al modelo, establezca el atributo [Remote] tal como se muestra en el ejemplo siguiente:

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields, al igual que todos los argumentos de atributo, debe ser una expresión constante. Por lo tanto, no use una cadena interpolada ni llame a Join para inicializar AdditionalFields.

Alternativas a los atributos integrados

Si necesita una validación que no proporcionan los atributos integrados, puede hacer lo siguiente:

Atributos personalizados

Para los escenarios que no se controlan mediante los atributos de validación integrados, puede crear atributos de validación personalizados. Cree una clase que herede de ValidationAttribute y reemplace el método IsValid.

El método IsValid acepta un objeto denominado value, que es la entrada que se va a validar. Una sobrecarga también acepta un objeto ValidationContext, que proporciona información adicional, como la instancia del modelo creada por el enlace de modelos.

El siguiente ejemplo valida que la fecha de lanzamiento de una película del género Classic no sea posterior a un año especificado. El atributo [ClassicMovie]:

  • Solo se ejecuta en el servidor.
  • En el caso de las películas clásicas, valida la fecha de lanzamiento:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
    {
        Year = year;
    }

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

La variable movie del ejemplo anterior representa un objeto Movie que contiene los datos del envío del formulario. Si se produce un error de validación, se devuelve un ValidationResult con un mensaje de error.

IValidatableObject

El ejemplo anterior solo funciona con tipos Movie. Otra opción para la validación del nivel de clase consiste en implementar IValidatableObject en la clase de modelo, como se muestra en el ejemplo siguiente:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}

Validación de nodo de nivel superior

Los nodos de nivel superior incluyen lo siguiente:

  • Parámetros de acción
  • Propiedades de controlador
  • Parámetros de controlador de página
  • Propiedades de modelo de página

Los nodos de nivel superior enlazados al modelo se validan además de la validación de las propiedades del modelo. En el ejemplo siguiente de la aplicación de muestra, el método VerifyPhone usa RegularExpressionAttribute para validar el parámetro de acción phone:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

Los nodos de nivel superior pueden usar BindRequiredAttribute con atributos de validación. En el ejemplo siguiente de la aplicación de muestra, el método CheckAge especifica que el parámetro age debe estar enlazado desde la cadena de consulta al enviar el formulario:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

En la página Comprobar edad (CheckAge.cshtml), hay dos formularios. El primer formulario envía un valor Age de 99 como una cadena de parámetro de consulta: https://localhost:5001/Users/CheckAge?Age=99.

Al enviar un parámetro age con un formato correcto desde la cadena de consulta, el formulario se valida.

El segundo formulario de la página Comprobar edad envía el valor Age en el cuerpo de la solicitud, y se produce un error de validación. Se produce un error en el enlace porque el parámetro age debe provenir de una cadena de consulta.

Número máximo de errores

La validación se detiene cuando se alcanza el número máximo de errores (200 de forma predeterminada). Puede configurar este número con el siguiente código en Startup.ConfigureServices:

services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

services.AddSingleton<IValidationAttributeAdapterProvider,
    CustomValidationAttributeAdapterProvider>();

Recursividad máxima

ValidationVisitor recorre el gráfico de objetos del modelo que se está validando. En el caso de los modelos profundos o infinitamente recursivos, la validación podría causar un desbordamiento de pila. MvcOptions.MaxValidationDepth proporciona una manera de detener pronto la validación si la recursividad del visitante supera la profundidad configurada. El valor predeterminado de MvcOptions.MaxValidationDepth es 32.

Cortocircuito automático

La validación cortocircuita (se omite) automáticamente si el gráfico de modelo no requiere validación. Entre los objetos para los que el tiempo de ejecución omite la validación se incluyen las colecciones de elementos primitivos (como byte[], string[] y Dictionary<string, string>) y gráficos de objeto complejo que no tienen los validadores.

Validación del lado cliente

La validación del lado cliente impide realizar el envío hasta que el formulario sea válido. El botón Enviar ejecuta JavaScript para enviar el formulario o mostrar mensajes de error.

La validación del lado cliente evita un recorrido de ida y vuelta innecesario en el servidor cuando hay errores de entrada en un formulario. Las referencias de script siguientes en _Layout.cshtml y _ValidationScriptsPartial.cshtml admiten la validación del lado del cliente:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.js"></script>

El script Validación discreta de jQuery es una biblioteca front-end personalizada de Microsoft que se basa en el conocido complemento Validación de jQuery. Si no usa Validación discreta de jQuery, deberá codificar la misma lógica de validación dos veces: una vez en los atributos de validación del lado servidor en las propiedades del modelo y luego en los scripts del lado cliente. En su lugar, los asistentes de etiquetas y los asistentes de HTML usan los atributos de validación y escriben metadatos de las propiedades del modelo para representar atributos data- HTML 5 para los elementos de formulario que necesitan validación. Validación discreta de jQuery analiza los atributos data- y pasa la lógica a la Validación de jQuery. De este modo, la lógica de validación del lado servidor se "copia" de manera eficaz en el cliente. Puede mostrar errores de validación en el cliente mediante el uso de asistentes de etiquetas, como se muestra aquí:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

Los anteriores asistentes de etiquetas representan el siguiente código HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

Tenga en cuenta que los atributos data- en los resultados HTML corresponden a los atributos de validación para la propiedad Movie.ReleaseDate. El atributo data-val-required contiene un mensaje de error que se muestra si el usuario no rellena el campo de fecha de estreno. La validación discreta de jQuery pasa este valor al método required() de la validación de jQuery, que luego muestra ese mensaje en el elemento <span> que lo acompaña.

La validación del tipo de datos se basa en el tipo .NET de una propiedad, a menos que lo reemplace un atributo [DataType]. Los exploradores tienen sus propios mensajes de error de predeterminados, pero el paquete de Validación discreta de jQuery Validate puede invalidar esos mensajes. Los atributos y las subclases [DataType], como [EmailAddress], permiten especificar el mensaje de error.

Validación discreta

Para obtener información sobre la validación discreta, consulte este problema de GitHub.

Agregar validación a formularios dinámicos

Validación discreta de jQuery pasa los parámetros y la lógica de validación a la Validación de jQuery cuando la página se carga por primera vez. Por lo tanto, la validación no funciona automáticamente en los formularios generados dinámicamente. Para habilitar la validación, hay que indicarle a Validación discreta de jQuery que analice el formulario dinámico inmediatamente después de su creación. Por ejemplo, en el código siguiente se configura la validación del lado cliente en un formulario agregado mediante AJAX.

$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})

El método $.validator.unobtrusive.parse() acepta un selector de jQuery para su único argumento. Este método indica a Validación discreta de jQuery que analice los atributos data- de formularios dentro de ese selector. Después, los valores de estos atributos se pasan al complemento de validación de jQuery.

Agregar validación a controles dinámicos

El método $.validator.unobtrusive.parse() funciona en todo el formulario, no en los controles individuales generados dinámicamente, como <input> y <select/>. Para volver a analizar el formulario, quite los datos de validación que se agregaron cuando el formulario se analizó anteriormente, como se muestra en el ejemplo siguiente:

$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

Validación del lado cliente personalizada

Para personalizar la validación del lado cliente, es necesario generar atributos HTML data- que funcionen con un adaptador personalizado de validación de jQuery. El siguiente ejemplo de código de adaptador se escribió para los atributos [ClassicMovie] y [ClassicMovieWithClientValidator] que se introdujeron anteriormente en este artículo:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

Para obtener información sobre cómo escribir adaptadores, vea la documentación de validación de jQuery.

El uso de un adaptador para un campo determinado se desencadena mediante atributos data- que:

  • Marcan que el campo está sujeto a validación (data-val="true").
  • Identifican un nombre de regla de validación y un texto de mensaje de error (por ejemplo, data-val-rulename="Error message.").
  • Proporcionan los parámetros adicionales que necesite el validador (por ejemplo, data-val-rulename-param1="value").

En el ejemplo siguiente se muestran los atributos data- para el atributo ClassicMovie de la aplicación de ejemplo:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

Como se indicó anteriormente, los asistentes de etiquetas y los asistentes de HTML usan información procedente de los atributos de validación para representar atributos data-. Hay dos opciones para escribir código que dé como resultado la creación de atributos HTML data- personalizados:

  • Puede crear una clase que derive de AttributeAdapterBase<TAttribute> y una clase que implemente IValidationAttributeAdapterProvider y, después, registrar el atributo y su adaptador en la inserción de dependencias. Este método sigue el principio de responsabilidad única, ya que el código de validación relacionado con servidor y el relacionado con el cliente se encuentran en clases independientes. El adaptador también cuenta con una ventaja: debido a que está registrado en la inserción de dependencias, tiene a su disposición otros servicios de la inserción de dependencias si es necesario.
  • Puede implementar IClientModelValidator en la clase ValidationAttribute. Este método es adecuado si el atributo no realiza ninguna validación en el lado servidor y no necesita ningún servicio de la inserción de dependencias.

AttributeAdapter para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovie en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  1. Cree una clase de adaptador de atributo para el atributo de validación personalizado. Derive la clase de AttributeAdapterBase<TAttribute>. Cree un método AddValidation que agregue atributos data- a la salida representada, tal como se muestra en este ejemplo:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(ClassicMovieAttribute attribute,
            IStringLocalizer stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext) =>
            Attribute.GetErrorMessage();
    }
    
  2. Cree una clase de proveedor de adaptador que implemente IValidationAttributeAdapterProvider. En el método GetAttributeAdapter, pase el atributo personalizado al constructor del adaptador, como se muestra en este ejemplo:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute,
            IStringLocalizer stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Registre el proveedor del adaptador para la inserción de dependencias en Startup.ConfigureServices:

    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    services.AddSingleton<IValidationAttributeAdapterProvider,
        CustomValidationAttributeAdapterProvider>();
    

IClientModelValidator para la validación del lado cliente

Este método para representar atributos data- en HTML es lo que usa el atributo ClassicMovieWithClientValidator en la aplicación de ejemplo. Para agregar la validación de cliente mediante este método:

  • En el atributo de validación personalizado, implemente la interfaz IClientModelValidator y cree un método AddValidation. En el método AddValidation, agregue atributos data- para la validación, como se muestra en el ejemplo siguiente:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
        {
            Year = year;
        }
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());
    
            var year = Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult IsValid(object value,
            ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

Deshabilitación de la validación del lado cliente

El código siguiente deshabilita la validación de cliente en las Razor Pages:

services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

Otras opciones para deshabilitar la validación del lado cliente:

  • Convierta en comentario la referencia a _ValidationScriptsPartial en todos los archivos .cshtml.
  • Quite el contenido del archivo Pages\Shared_ValidationScriptsPartial.cshtml.

El enfoque anterior no impedirá la validación del lado cliente de la biblioteca de clases de IdentityRazor de ASP.NET Core. Para obtener más información, vea Scaffolding Identity en proyectos de ASP.NET Core.

Recursos adicionales