ASP.NET MVC – Patrón PRG sin sesión

Buenas! El patrón PRG (Post – Redirect – Get) es un patrón muy usado en el desarrollo web. Consiste en que la respuesta de una petición POST es siempre una redirección, lo que genera un GET del navegador y de ahí el nombre.

La idea que subyace tras el patrón PRG es, que dado que dado que las peticiones GET son (¡deberían ser!) idempotentes esas son las únicas que el usuario debe poder refrescar. De hecho los navegadores nos avisan si refrescamos una petición POST:

image

La razón de este aviso no es tanto notificar al usuario que se reenviarán esos datos, la razón es que el hecho de que se envíen via POST hace que se asuma que dicha petición no es idempotente, o dicho de otro modo modifica datos en el sistema (da de alta un usuario, o borra un producto o realiza una compra). Es pues un mecanismo de protección.

Para evitar esto en el patrón PRG cada método de acción que gestiona un POST, no devuelve la vista con el resultado de dicha petición si no que devuelve una redirección a otra acción que es la que muestra el resultado. Así si tenemos una vista que tiene un formulario como el siguiente:

@using (Html.BeginForm())
{
    @Html.TextBox("somevalue")
    <input type="submit" value="send post" />
}

El método de acción podría ser algo como:

[HttpPost]
public ActionResult Index(string somevalue)
{
    // Procesar resultados.
    var id = new Random().Next();
    // ...
    return RedirectToAction("View", new { id = id });
}

El método que procesa el POST realiza las tareas que sean necesarias y luego redirecciona a otra acción, por lo que lo después de enviar el formulario el usuario refresca la página, refrescará la última petición web que es la petición GET (en lugar de intentar refrescar el POST).

Usar el patrón PRG es una muy buena práctica, pero conlleva un pequeño problema: como pasar información desde la acción POST hacia la acción GET.

P. ej. en el POST podemos crear o modificar datos de alguna entidad y luego en el GET podemos mostrar esa entidad creada o modificada. Una solución es pasarle el ID de la entidad (tal y como se hace en el código anterior). Eso es perfectamente válido pero implica un round-trip a la BBDD. En el POST teníamos los datos de toda la entidad, pero en el GET debemos recuperarlos de nuevo ya que solo tenemos el ID.

Si hubiese alguna manera de pasar todos los datos necesarios (p. ej. toda la entidad) des del POST hacía el GET entonces, en algunos casos, nos podríamos ahorrar tener que ir a buscar datos que ya teníamos.

En ASP.NET MVC existe un mecanismo pensado para transferir datos entre redirecciones, que es justo lo que necesitamos y es TempData:

[HttpPost]
public ActionResult Index(string somevalue)
{
    // Procesar resultados.
    var id = new Random().Next();
    var someData = new Person() { 
       Id = id, Name = somevalue 
     };
    TempData["someData"] = someData;
    return RedirectToAction("View", 
          new { id = id });
}
 
[ActionName("View")]
public ActionResult ViewGet(string id)
{
    var data = TempData["someData"] as Person;
    if (data == null)
    {
        // Recargar data usando el id que 
        //tenemos por parámetro
    }
    return View(data);
}

Fíjate que usar TempData no exime de pasar el id igualmente a la acción GET ya que los datos que recogemos de TempData pueden no existir. Si el usuario refresca la página o bien teclea directamente la URL de la acción View los datos de TempData no existirán. Recuerda que TempData es un contenedor que permite una sola lectura (los datos desaparecen una vez leídos).

Bien, el punto a tener en cuenta (y motivo principal de este post) al usar TempData es que éste usa sesión. Y no siempre podemos o queremos usar la sesión. Si por cualquier razón no queremos usar la sesión, ¿podemos seguir usando TempData?

La respuesta es que si, pero debes implementarte un custom TempData provider. Claro que la otra pregunta es donde podemos guardar esos datos. Recuerda que TempData debe persistir entre peticiones (de ahí que por defecto se use la sesión). No hay muchos lugares más donde lo podamos guardar, así que si estás pensando en cookies has dado en el clavo. Si en el POST enviamos una cookie, cuando el navegador realice la petición GET posterior reenviará la cookie que contendrá los datos de TempData.

Para crear un proveedor propio de TempData tenemos que seguir 2 pasos:

  1. Crear una clase que implemente ITempDataProvider. Esta interfaz define dos métodos (SaveTempData y LoadTempData).
  2. Redefinir el método CreateTempDataProvider de la clase Controller y devolver una instancia de nuestra clase que implementa ITempDataProvider. Esto debemos hacerlo en cada controlador que queramos que use nuestro TempData provider o bien lo podemos poner en un controlador base.

No voy a colgarme medallas que no me pertenecen poniendo la implementación de un proveedor de TempData que use cookies, ya que hay varios por internet e incluso en el código fuente de MVC4 viene uno. Está en el código fuente pero no en los binarios, ya que forma parte del paquete Mvc4Futures. Si quieres usarlo, debes instalar primero este paquete usando Install-Package Mvc4Futures desde la cónsola de NuGet o bien usando la GUI.

Y ya puedes usar TempData sin necesidad de usar la sesión! 😉

Saludos!

3 comentarios en “ASP.NET MVC – Patrón PRG sin sesión”

  1. Hola Eduard,

    Hoy estoy preguntón:

    He visto que la implementación del custom TempDataProvider utiliza serialización binaria y luego a base64. Si mal no recuerdo esto obliga a que aquello que queramos serializar esté decorado con el atributo Serializable (incluido un tipo anónimo como el que utilizas en el ejemplo). Lo mismo Mvc4Futures utiliza otro serializador…

    Por otro lado, el tema de Mvc4Futures son características que están por venir en MVC, no? ¿Es seguro utilizarlo ahora? Es decir, aunque se llame “Future” entiendo que no son implementaciones betas, ni cosas así…

    Por último ¿Podríamos decir que deberíamos utilizar el patrón PRG para cualquier controlador/acción que tenga un POST de por medio? Al final si lo llevas al límite con el tema de que un usuario siempre puede pulsar F5, parece la única forma válida de cubrirte totalmente las espaldas

    Genial artículo y perdona que sea tan preguntón 😉

  2. Buenas!
    Puntualizar que en el ejemplo no uso un tipo anónimo, uso una clase Person (si no desde la otra acción no podría recuperar el valor de TempData, pues no tendría nada a que convertirlo). 🙂 Tienes razón con lo que comentas del [Serializable] pero esto no debería ser un impedimento demasiado importante. El proveedor de TempData basado en cookies de Mvc4Futures es este mismo 🙂

    MvcFutures NO son características en fase beta que vayan a venir en futuras versiones de MVC. Son características que el equipo de ASP.NET MVC no considera necesarias ponerlas en la versión “oficial” (y por tanto darles soporte). Algunas de esas características (como el Value Provider de JSON) han pasado de MvcFutures a ASP.NET MVC y otras no lo han hecho.

    Y respecto a la última pregunta: Deberíamos usar PRG para protegernos de todos los POSTs? En mi opinión sí: todo lo que sea ayudar a evitar que el usuario pueda realizar cambios no intencionados en nuestro sistema es bueno! Por supuesto esto no aplica a llamadas ajax, ya que esas no son “refrescables” por el usuario.

    Un saludo y gracias por tu comentario!!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *