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

De Kirk Larkin

En este artículo se explica cómo validar la entrada del usuario en una aplicación ASP.NET Core MVC Razor o 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 de modelos como la validación del modelo se producen antes de la ejecución de una acción de controlador o un Razor método de controlador de 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 ModelState.IsValid si 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 para borrar la ModelStateDictionary.ClearValidationState validación específica del modelo que se va a validar seguido 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. [ClassicMovieWithClientValidator] no se muestra. [ClassicMovieWithClientValidator] muestra 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]: ValidateNeverAttribute indica que una propiedad o parámetro debe excluirse de la validación.
  • [CreditCard]: valida que la propiedad tiene un formato de tarjeta de crédito. Requiere métodos adicionales de validación de jQuery.
  • [Compare]: valida que dos propiedades de un modelo coinciden.
  • [EmailAddress]: valida que la propiedad tiene un formato de correo electrónico.
  • [Phone]: valida que la propiedad tiene un formato de número de teléfono.
  • [Range]: valida que el valor de propiedad se encuentra dentro de un intervalo especificado.
  • [RegularExpression]: valida que el valor de propiedad coincide con una expresión regular especificada.
  • [Required]: valida que el campo no es NULL. Consulte [Required] el atributo para obtener más información sobre el comportamiento de este atributo.
  • [StringLength]: valida que un valor de propiedad de cadena no supera un límite de longitud especificado.
  • [Url]: valida que la propiedad tiene un formato de dirección URL.
  • [Remote]: valida la entrada en el cliente mediante una llamada a un método de acción en el servidor. Consulte [Remote] el atributo 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 [Obligatorio]

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

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

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

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). Acepta <T> valores NULL Los tipos de valor 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 remoto 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; }
    

    El atributo [Remote] está en el espacio de nombres Microsoft.AspNetCore.Mvc.

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 siguientes referencias de script de _Layout.cshtml y _ValidationScriptsPartial.cshtml admiten la validación del lado cliente:

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

El script de validación discreta de jQuery es una biblioteca de front-end de Microsoft personalizada que se basa en el popular complemento de 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. La validación discreta de jQuery analiza los atributos y pasa la lógica a la validación data- de jQuery, "copiando" de forma eficaz la lógica de validación del lado servidor al 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. jQuery Unobtrusive Validation pasa este valor al método jQuery Validation required(), que luego muestra ese mensaje en el elemento que lo <span> 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

La validación discreta de jQuery pasa la lógica de validación y los parámetros 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. A continuación, los valores de esos 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

La validación del lado cliente personalizada se realiza mediante la generación de data- atributos HTML que funcionan con un adaptador de validación de jQuery personalizado. 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 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 ASP.NET Core Identity Razor biblioteca de clases. Para obtener más información, vea Scaffolding Identity en ASP.NET Core proyectos.

Recursos adicionales

En este artículo se explica cómo validar la entrada del usuario en una aplicación ASP.NET Core MVC Razor o 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 en el enlace de modelos suelen ser errores de conversión de datos (por ejemplo, se especifica una "x" en un campo que espera un entero). La validación de modelos se produce después de enlace de modelos y notifica errores cuando los datos no se ajustan a las reglas de negocios (por ejemplo, se especifica un 0 en un campo que espera una clasificación entre 1 y 5).

Tanto el enlace de modelos como la validación se producen antes de la ejecución de una acción de controlador o un Razor método de controlador 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.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Los controladores de Web API no tienen que comprobar ModelState.IsValid si 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 al método TryValidateModel, como se muestra aquí:

var movie = new Movie
{
    Title = title,
    Genre = genre,
    ReleaseDate = modifiedReleaseDate,
    Description = description,
    Price = price,
    Preorder = preorder,
};

TryValidateModel(movie);

if (ModelState.IsValid)
{
    _context.AddMovie(movie);
    _context.SaveChanges();

    return RedirectToAction(actionName: nameof(Index));
}

return View(movie);

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 [ClassicMovie2], 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)]
    public DateTime ReleaseDate { get; set; }

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

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

    [Required]
    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

Atributos integrados

Entre los atributos de validación integrados se incluyen:

  • [CreditCard]: valida que la propiedad tiene un formato de tarjeta de crédito.
  • [Compare]: valida que dos propiedades de un modelo coinciden. Por ejemplo, el archivo Register.cshtml.cs usa [Compare] para validar que ambas contraseñas escritas coincidan. Scaffolding Identity para ver el código de registro.
  • [EmailAddress]: valida que la propiedad tiene un formato de correo electrónico.
  • [Phone]: valida que la propiedad tiene un formato de número de teléfono.
  • [Range]: valida que el valor de propiedad se encuentra dentro de un intervalo especificado.
  • [RegularExpression]: valida que el valor de propiedad coincide con una expresión regular especificada.
  • [Required]: valida que el campo no es NULL. Consulte [Required] el atributo para obtener más información sobre el comportamiento de este atributo.
  • [StringLength]: valida que un valor de propiedad de cadena no supera un límite de longitud especificado.
  • [Url]: valida que la propiedad tiene un formato de dirección URL.
  • [Remote]: valida la entrada en el cliente mediante una llamada a un método de acción en el servidor. Consulte [Remote] el atributo para obtener más información sobre el comportamiento de este atributo.

Cuando se usa el atributo [RegularExpression] con la validación del lado cliente, la regex se ejecuta en JavaScript en el cliente. Esto significa que se usará el comportamiento de coincidencia de ECMAScript. Para más información, consulte este problema de GitHub.

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.

Atributo [Required]

De forma predeterminada, el sistema de validación trata las propiedades o los parámetros que no aceptan valores NULL como si tuvieran un atributo [Required(AllowEmptyStrings = true)]. Los tipos de valor como decimal y int no aceptan valores NULL.

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). Acepta <T> valores NULL Los tipos de valor 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.AddMvc(options => 
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                (_) => "The field is required.");
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    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 jQuery Validate 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 (!_userRepository.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; }
    

    El atributo [Remote] está en el espacio de nombres Microsoft.AspNetCore.Mvc. Instale el paquete de NuGet Microsoft.AspNetCore.Mvc.ViewFeatures si no usa Microsoft.AspNetCore.App o el metapaquete Microsoft.AspNetCore.All.

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))]
public string FirstName { get; set; }
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
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 de nombre y apellido:

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userRepository.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 [ClassicMovie2] comprueba primero el género y continúa únicamente si es Classic. En el caso de las películas identificadas como clásicas, comprueba la fecha de lanzamiento para asegurarse de que no es posterior al límite pasado al constructor de atributos.

public class ClassicMovieAttribute : ValidationAttribute
{
    private int _year;

    public ClassicMovieAttribute(int year)
    {
        _year = 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;
    }

    public int Year => _year;

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

La variable movie del ejemplo anterior representa un objeto Movie que contiene los datos del envío del formulario. El método IsValid comprueba la fecha y el género. Si la validación es correcta, IsValid devuelve un código ValidationResult.Success. 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 MovieIValidatable : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

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

    [Required]
    public DateTime ReleaseDate { get; set; }

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

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

    [Required]
    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 earlier than {_classicYear}.",
                new[] { "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 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.

Cuando se ejecuta con CompatibilityVersion.Version_2_1 o versiones posteriores, la validación de nodo de nivel superior está habilitada de forma predeterminada. En caso contrario, la validación del nodo de nivel superior está deshabilitada. La opción predeterminada se puede invalidar si se establece la propiedad AllowValidatingTopLevelNodes en (Startup.ConfigureServices), como se muestra aquí:

services.AddMvc(options => 
    {
        options.MaxModelValidationErrors = 50;
        options.AllowValidatingTopLevelNodes = false;
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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.AddMvc(options => 
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            (_) => "The field is required.");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
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 muy 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 cuando se ejecuta con CompatibilityVersion.Version_2_2 o una versión posterior. Para las versiones anteriores, el valor es NULL, lo que significa que no hay restricción de profundidad.

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.

Deshabilitación de la validación

Para deshabilitar la validación:

  1. Cree una implementación de IObjectModelValidator que no marque ningún campo como no válido.

    public class NullObjectModelValidator : IObjectModelValidator
    {
        public void Validate(
            ActionContext actionContext,
            ValidationStateDictionary validationState,
            string prefix,
            object model)
        {
        }
    }
    
  2. Agregue el código siguiente a Startup.ConfigureServices para reemplazar la implementación predeterminada de IObjectModelValidator en el contenedor de inserción de dependencias.

    // There is only one `IObjectModelValidator` object,
    // so AddSingleton replaces the default one.
    services.AddSingleton<IObjectModelValidator>(new NullObjectModelValidator());
    

Es posible que todavía vea errores de estado del modelo originados en el enlace de modelos.

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 siguientes referencias de script de _Layout.cshtml y _ValidationScriptsPartial.cshtml admiten la validación del lado cliente:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.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 jQuery Validate. 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="ReleaseDate" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="ReleaseDate" class="form-control" />
        <span asp-validation-for="ReleaseDate" class="text-danger"></span>
    </div>
</div>

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

<form action="/Movies/Create" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <div class="text-danger"></div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
            <div class="col-md-10">
                <input class="form-control" type="datetime"
                data-val="true" data-val-required="The ReleaseDate field is required."
                id="ReleaseDate" name="ReleaseDate" value="">
                <span class="text-danger field-validation-valid"
                data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
            </div>
        </div>
    </div>
</form>

Tenga en cuenta que los atributos data- en los resultados HTML corresponden a los atributos de validación para la propiedad 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. jQuery Unobtrusive Validation pasa este valor al método jQuery Validate required(), que luego muestra ese mensaje en el elemento que lo <span> 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.

Agregar validación a formularios dinámicos

Validación discreta de jQuery pasa los parámetros y la lógica de validación a jQuery Validate 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 jQuery Validate.

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 Validate
               .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 jQuery Validate. El siguiente ejemplo de código de adaptador se escribió para los atributos ClassicMovie y ClassicMovie2 que se introdujeron anteriormente en este artículo:

$.validator.addMethod('classicmovie',
    function (value, element, params) {
        // Get element value. Classic genre has value '0'.
        var genre = $(params[0]).val(),
            year = params[1],
            date = new Date(value);
        if (genre && genre.length > 0 && genre[0] === '0') {
            // Since this is a classic movie, invalid if release date is after given year.
            return date.getUTCFullYear() <= year;
        }

        return true;
    });

$.validator.unobtrusive.adapters.add('classicmovie',
    ['year'],
    function (options) {
        var element = $(options.form).find('select#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 jQuery Validate.

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-parm1="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="datetime"
    data-val="true"
    data-val-classicmovie1="Classic movies must have a release year earlier than 1960."
    data-val-classicmovie1-year="1960"
    data-val-required="The ReleaseDate field is required."
    id="ReleaseDate" name="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>
    {
        private int _year;
    
        public ClassicMovieAttributeAdapter(ClassicMovieAttribute attribute, IStringLocalizer stringLocalizer) : base (attribute, stringLocalizer)
        {
            _year = attribute.Year;
        }
        public override void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(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)
        {
            return 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
    {
        IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
        public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute,
            IStringLocalizer stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
            else
            {
                return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
            }
        }
    }
    
  3. Registre el proveedor del adaptador para la inserción de dependencias en Startup.ConfigureServices:

    services.AddMvc(options => 
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                (_) => "The field is required.");
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    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 ClassicMovie2 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 ClassicMovie2Attribute : ValidationAttribute, IClientModelValidator
    {
        private int _year;
    
        public ClassicMovie2Attribute(int year)
        {
            _year = 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;
        }
    
        public void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(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);
        }
        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
        protected string GetErrorMessage()
        {
            return $"Classic movies must have a release year no later than {_year} [from attribute 2].";
        }
    }
    

Deshabilitación de la validación del lado cliente

El código siguiente deshabilita la validación de cliente en las vistas de MVC:

services.AddMvc().AddViewOptions(options =>
{
    if (_env.IsDevelopment())
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    }
});

Y en Razor Pages:

services.Configure<HtmlHelperOptions>(o => o.ClientValidationEnabled = false);

Otra opción para deshabilitar la validación de cliente consiste en marcar como comentario la referencia a _ValidationScriptsPartial en el archivo .cshtml.

Recursos adicionales