Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 1 de diciembre de 2009

Redirección basada en retornar un HTTP 302ASP.NET MVC ofrece “de serie” mecanismos para transferir el control desde una acción a otra utilizando para ello redirecciones HTTP. Esto significa que cuando un cliente realiza una petición y el método de acción desea que ésta sea procesada desde otra acción, el navegador será informado de la URL a la que debe dirigirse mediante una respuesta de tipo 302 para que, tras recibirla, realice una nueva solicitud que finalmente ejecutará la lógica apropiada.

En el diagrama de la derecha se muestra el proceso completo de transferencia desde una petición a /home/mail hacia /mail/index utilizando la ruta por defecto, y muestro una posible implementación en ASP.NET MVC a continuación:

public class HomeController : Controller
{
    [...]
    // GET /Home/Mail
    public ActionResult Mail()
    {
        return RedirectToAction("index", "mail");
    }
 
}
 
public class MailController : Controller
{
    [...]
    // GET /Mail
    public ActionResult Index()
    {
        return View();
    }
 
}

Aunque es la forma habitual de realizar redirecciones en ASP.NET MVC, hace unos días estaba pensando que hay ocasiones en las tanta petición puede resultar pesada y vendría bien disponer de algo parecido al Server.Transfer, presente desde que peleábamos con ASP clásico, que era capaz de transferir la ejecución a otro punto sin necesidad de que el cliente replanteara la petición.

Y no es que me parezca una práctica especialmente recomendable, puesto que hay muchas cuestiones de ese tipo que pueden resolverse en MVC a nivel configuración de rutas o simplemente replanteando la estructura de direcciones del sitio, pero la verdad es que cuando te pica el gusanillo…

Intento erróneo #1: invocar directamente una acción desde otra

Bueno, en realidad este intento no llegué a hacerlo, pero se trata de un error bastante frecuente cuando se empieza con ASP.NET MVC framework, y me ha parecido interesante reflejarlo aquí como aviso a navegantes:

public class HomeController : Controller
{
    // GET /Home/Mail
    public ActionResult Mail()
    {
        return new MailController().Index(); // <- Mal
    }
 
}

Aunque a primera vista parece tener sentido, y de hecho funcionará muchas veces, se trata de una mala práctica y puede causarnos muchos problemas, y sobre todo, como en el ejemplo anterior, si se trata de llamadas de acciones de distintos controladores. Hay que tener en cuenta que en el contexto de petición estará la información de la petición inicial (/home/mail), no el que podría esperarse desde el método de acción new MailController().Index().

También es peligroso porque al invocarlo directamente estaríamos saltándonos todos los filtros que hubiéramos podido establecer mediante atributos en la acción final, MailController.Index(). Imaginad, por ejemplo, que la acción está protegida por un [Authorize]… :-O

Intento erróneo #2: invocar a Server.Transfer en el controlador

El segundo intento fue, pues eso, invocar directamente a Server.Transfer() desde el cuerpo de una acción. Pensaba que en cualquier caso no era una buena idea puesto que el método de acción que lo invocase sería difícilmente comprobable usando pruebas unitarias, pero bueno, estaba dispuesto a sacrificar esta ventaja:

public class HomeController : Controller
{
    // GET /Home/Mail
    public ActionResult Mail()
    {
        Server.Transfer(Url.Action("Index", "Mail")); // Mal
        return new EmptyResult();                     // En realidad podría devolver
    }                                                 // un nulo, o cualquier otra cosa
}

Al ejecutar el proyecto, el resultado obtenido es una bonita excepción “Error al ejecutar la solicitud secundaria” al intentar procesar la transferencia hacia la dirección “/Mail” (generada por el helper Url.Action()).

Primer acercamiento a la solución correcta: default.aspx

Aparentemente, la solución para transferir el control hacia otra acción pasa por recorrer el ciclo de vida completo de la petición, pero sin necesidad de que el navegador vuelva a hacerla. Ahora la cuestión es cómo conseguir esto armando el menor estropicio posible.

Si os fijáis, al crear el proyecto ASP.NET MVC, en la plantilla se habrá incluido un archivo en la carpeta raíz del mismo, llamado default.aspx. Su objetivo es transferir la ejecución al framework MVC si un usuario realiza una petición al raíz del sitio, siendo esta página el documento por defecto.

Observemos el código por defecto del método Page_Load que encontramos en su code-behind:

public void Page_Load(object sender, System.EventArgs e)
{
    // Change the current path so that the Routing handler can correctly interpret
    // the request, then restore the original path so that the OutputCache module
    // can correctly process the response (if caching is enabled).
 
    string originalPath = Request.Path;
    HttpContext.Current.RewritePath(Request.ApplicationPath, false);
    IHttpHandler httpHandler = new MvcHttpHandler();
    httpHandler.ProcessRequest(HttpContext.Current);
    HttpContext.Current.RewritePath(originalPath, false);
}

Como indican los comentarios, lo que se hace es cambiar la ruta original de la petición, instanciar un nuevo manejador MVC (MvcHttpHandler) y hacer que éste se encargue de procesarla… o en otras palabras, estamos transfiriendo la ejecución, justo lo que queríamos conseguir. Después, para evitar daños colaterales, se vuelve a dejar la ruta de la petición como estaba.

Por tanto, podríamos utilizar este mismo código y crear un método de extensión sobre la clase Controller:

public static class ControllerExtensions
{
    public static void Transfer(this Controller controller, string url)
    {
        string originalPath = controller.Request.Path;
        HttpContext.Current.RewritePath(url, false);
        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest(HttpContext.Current);
        HttpContext.Current.RewritePath(originalPath, false);
    }
}

Así, podríamos utilizarlo desde nuestras acciones de la siguiente forma:

public class HomeController : Controller
{
    [...]
 
    // GET /Home/Mail
    public ActionResult Mail()
    {
        this.Transfer(Url.Action("index", "mail"));
        return null;
    }
}

Con esto ya tenemos una primera solución a nuestro problema. Sin embargo, aunque puede parecer muy limpio, y de hecho yo estaba bastante conforme con esta solución, este código tiene varios problemas.

En primer lugar, realizar pruebas unitarias sobre la acción Mail sería realmente complicado. Además, la invocación a Transfer() ejecutaría la acción asociada a la ruta de la URL indicada, pero al finalizar continuaría ejecutándose el método Mail(), desde donde la hemos invocado. Esto puede provocar efectos curiosos, si por ejemplo retornásemos un ViewResult o cualquier otro tipo de contenido desde la misma (serían enviados de forma consecutiva el cliente).

Solución final: crear un ActionResult personalizado

Aunque el enfoque anterior es correcto y podría ser válido a pesar de sus inconvenientes, encuentro una solución más fina en una pregunta de StackOverflow, How to simulate Server.Transfer in ASP.NET MVC: crear un resultado de acción (ActionResult) personalizado.

/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : RedirectResult
{    
    public TransferResult(string url): base(url)    {    }
   
    public override void ExecuteResult(ControllerContext context)    
    {        
        var httpContext = HttpContext.Current;        
        httpContext.RewritePath(Url, false);        
        IHttpHandler httpHandler = new MvcHttpHandler();        
        httpHandler.ProcessRequest(HttpContext.Current);    
    }
}

De esta forma, evitamos los dos problemas de la anterior aproximación. El método de acción seguiría siendo fácilmente testeable, y evitaríamos resultados indeseados al tratarse de un tipo de respuesta, que evita la posibilidad de introducir código adicional.

Publicado en: Variable not found.

Aún no hay comentarios, ¡sé el primero!