Agregando validación a una aplicación ASP.NET Core MVC

En esta sección vamos a agregar lógica de validación al modelo Movie, y vamos a asegurarnos que las reglas de validación se cumplen cada vez que un usuari oedita o crea una película.

Manteniendo las cosas DRY

Uno de los principios de diseño de MVC es DRY (Don't Repeat Yourself). ASP.NET MVC nos incentiva a que especifiquemos la lógica o el comportamiento una sola vez y luego lo veamos reflejado en toda la aplicación. Esto reduce la cantidad de código que tenemos que escribir y hace que el código que escribamos sea menos propenso a los errores, facil de probar y más facil de mantener.

Un ejemplo de esto es el soporte a la validación que MVC y Entity Framework Core Code First nos proveen. Podemos especificar de manera declarativa las reglas de validación en un solo lugar (en la clase de modelo) y las reglas se respetan en toda la aplicación.

Agregando las reglas de validación al modelo de película

Abramos el archivo Movie.cs. Las DataAnnotations nos proveen un conjunto de validaciones que vienen integradas y que podemos aplicar de manera declarativa a cualquier clase o propiedad. (también contiene atributos de formateo como el DateType que nos ayudan con el formateo y no proveen ninguna validación).

Actualicemos la clase Movie para sacar ventaja de estos atributos de validación Required, StringLength, RegularExpresion y Range.

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

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; }

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

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; }
}

Los atributos de validación especifican el comportamiento que queremos hacer cumplir en las propiedades de modelo que son aplicados. Los atributos Required y MinimunLength indican que una propiedad debe tener un valor; pero nada evita que un usuario satisfaga esta validación ingresando un espacio en blanco. El atributo RegularExpression se utiliza para limitar que caracteres pueden ser ingresados. En el código de arriba, Genrey Rating solamente pueden usar letras (Primera letra debe ser en en mayúsculas, espacio en blanco, números y caracteres especiales no estan permitidos). El atributo Range restringe un valor al rango especificado. El atributo StringLength te deja configurar un tamaño máximo para una propiedad string, y opcionalmente, un tamaño mínimo. Los tipos de valor (tales como decimal, int, float, DateTime) son inheremente requeridos y no necesitan el atributo [Required].

Tener las reglas de validación controladas automáticamente por ASP.NET nos ayuda a hacer nuestra aplicación más robusta. Además, nos asegura que no nos olvidemos de validar algo y terminemos con datos incorrectos en nuestra base de datos.

La UI de los errores de validación en MVC

Ejecutemos la aplicación y naveguemos al controlador de películas.

Toquemos en el enlace Create new para agregar una nueva película. Llenemos los detalles del formulario con algunos valores incorrectos. Tan pronto como la validación del lado del cliente provista por jQuery detecta el error, lo muestra con un mensaje de error.

Los mensajes de error de validación

Nota:

Es posible que no podamos ingresar números decimales en el campo Precio. Para dar soporte a la validación de jQuery en configuraciones regionales que usan una coma (",") para un punto decimal, y formatos de fecha distintos al de Estados Unidos en inglés, debemos realizar unos pasos para globalizar nuestra aplicación. Esta incidencia de GitHub contiene instrucciones para agregar la coma decimal.

Notemos como el formulario se ha renderizado automáticamente agregando el error de validación en cada campo que contenga un valor incorrecto. Los errores se controlan tanto del lado del cliente (usando JavaScript y jQuery) como del lado del servidor (en este caos este usuario tiene JavaScript desactivado).

Un beneficio significativo es que no necesitamos cambiar una sola línea de código en la clase MovieController o en el archivo Create.cshtml para activar esta UI de validación. El controlador y las vistas que creamos previamente en este tutorial automáticamente han tomado las reglas de validación que hemos especificado en las propiedades de la clase de modelo Movie. Probemos la validación utilizando el método de acción Edit, la misma validación es aplicada.

Los datos del formulario no se envían al servidor hasta que no se hayan corregido los errores de validación. Podemos verificar esto poniendo un punto de interrupción, o usando Fiddler, o las herramientas para desarroladores F12

Como funciona la validación

// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    [Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(movie);
}

El primer método de acción Create (HTTP GET) muestra el formulario inicial para crear películas.

El segundo método de acción Create (la versión HTTP POST) llama a ModelState.IsValid para verificar si la película tiene algún error de validación. Llamar a a este método evalúa cualquier atributo de validación que hayamos aplicado sobre el objeto. Si el objeto tiene errores de validación el método Create va a volver a mostrar el formulario para que el usuario vuelva a ingresar los datos. Si no hay errores, el método guarda la película nueva en la base de datos. En nuestro ejemplo de películas, el formulario no se postea al servidor si hubiese algun error de validación detectectado del lado del cliente. El segundo método Create no se llama nunca cuando hay errores de validación del lado del cliente. Si desactivamos el JavaScript en nuestro navegador, la validación del cliente se desactiva y podemos testear el método HTTP POST Create y el ModelState.IsValid para detectar cualquier error de validación.

La siguiente imagen muestra como desactivar JavaScript en el navegador Firefox.

Desactivar JavaScript en el navegador Firefox

La siguiente imagne muestra como desactivar JavaScript en el navegador Chrome.

Desactivar JavaScript en el navegador Chrome

Después de desactivar JavaScript, probemos postear datos incorrectos y ejecutar paso a paso con el depurador.

Depurador ejecutando paso a paso con el ModelState.IsValid resaltado

Más abajo está la porción del Create.cshtml que generamos antes en el tutorial, usando scaffolding. Se usa por los métodos de acción mostrados arriba para mostrar el formulario inicial y volverlo a mostrar en el evento de un error.

<form asp-action="Create">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />

        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Title" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
        </div>

        @*Markup removed for brevity.*@
    </div>
</form>

El Tag Helper de Input utiliza los atributos de las DataAnnotations y produce HTML necesarios para la validación de jQuery en el lado del cliente. El Tag Helper de Validación muestra los errores de validación. Ver Validación para más información.

Lo que es realmente bueno acerca de este enfoque es que ni el controlador ni la vista Create saben nada acerca de las reglas de validación que se van a controlar o de los mensajes de error específicos que se van a mostrar. Las reglas de validación y las cadenas de error se especifican solamente en la clase Movie. Estas mismas reglas de validación se aplican automáticamente a la vista Edit y a cualquier otras vistas que vayamos a crear que editen nuestro modelo.

Cuando necesitamos cambiar la lógica de validación, podemos hacerlo en un solo lugar agregando atributos de validación al modelo (en este ejemplo, en la clase Movie). No tenemos que preocuparnos de que las diferentes partes de nuestra aplicación vayan a quedar inconsistentes con como se aplican estas reglas. Toda la lógica de validación se va a definir en un solo lugar, y usar en todos lados. Esto mantiene el código muy limpio y lo hace facil de mantener y evoluciónar. Y eso significa que le estaremos haciendo honor al principio DRY.

Usando atributos DataType

Abaramos el archivo Movie.cs y examinemos la clase Movie. El espacio de nombres System.ComponentModel.DataAnnotations provee información de atributos de formateo además de una serie de atributos de validación que vienen incorporados. Ya hemos aplicado la enumeración DataType para los campos ReleaseDate y Date. El código siguiente muestra las propiedades ReleaseDate y Price con el valor apropiado para el atributo DataType.

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

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }

Los atributos DataType solamente proveen ayudas para el motor de vistas para formatear los datos (y proveer elementos/atributos como el <a> para una URL y el <a href="mailto:EmailAddress.com"> para un email). Puedes usar el atributo RegularExpression para validar el formato de los datos. El atributo DataType se usa para especificar algun tipo de datos distinto al de tipo intrínseco de la base de datos, no son atributos de validación. La enumeración DataType provee de muchos tipos de datos, por ejemplo fecha, hora, número de telefono, moneda, dirección de email y más. El atributo DataType tambien activa la aplicación para que provea de manera automática a características especificas de tipo. Por ejemplo un enlace mailto: se puede crear para DataType.EmailAddress, y un selector de fecha se puede proveer para DataType.Date en los navegadores que soportan HTML5. Los atributos DataType emiten atributos data- HTML5 que los navegadores modernos pueden entender. De nuevo, los atributos DataType no proveen ninguna validación.

DataType.Date no especifica el formato de la fecha que se va a mostrar. Por defecto, el campo de datos se muestra de acuerdo a los formatos por defecto basados en la configuración CultureInfo del servidor.

El atributo DisplayFormat se usa para configurar de manera específica los formatos de fecha:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }

La configuración ApplyInEditMode especifica que el formateo también se debe aplicar cuando el valor se muestre en un text box para editar. (Puede que no quieras esop para algunos campos, por ejemplo, valores de moneda en los que no queremos que el símbolo de la moneda esté en la edición).

Podemos usar el atributo DisplayFormat por si mismo, pero por lo general es mejor idea usar el atributo DataType. El atributo DataType expresa la semántica de los datos en lugar de decir como van a ser mostrados en la pantalla y provee los siguientes beneficios que no vamos a tener con DisplayFormat:

  • El navegador puede activar las características HTML5 (por ejemplo mostrar un calendario, o el formato de monedas correcto, o enlaces de email, etc).
  • Por defecto, el navegador va a renderizar los datos utilizando el formato correcto basados en la configuración regional.
  • El atributo DataType puede ayudar a MVC a elegir la mejor plantilla para renderizar los datos (si solo usamos DisplayFormat solamente usa el string template).

Nota: La validación de jQuery no funciona con el atributo Range si estamos usando un DateTime. Por ejemplo, el siguiente código siempre va a mostrar un error de validación a pesar de que la fecha esté en el rango especificado.

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

Vamos a tener que desactivar la validación de jQuery para usar el atributo Range con un DateTime. Generalmente no se recomienda hardcodear fechas en nuestros modelos, por lo que se desancoseja utilizar el atributo Range con un DateTime.

El siguiente código combina los atributos en una sola línea:

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

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }

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

    [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$"), Required, StringLength(30)]
    public string Genre { get; set; }

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; }
}

En la siguiente parte del tutorial vamos a revisar la aplicación y hacer unas mejoras a los métodos generados Details y Delete