¿Cómo simular Server.Transfer en ASP.NET MVC?

124

En ASP.NET MVC puede devolver un ActionResult de redireccionamiento con bastante facilidad:

 return RedirectToAction("Index");

 or

 return RedirectToRoute(new { controller = "home", version = Math.Random() * 10 });

Esto realmente dará una redirección HTTP, que normalmente está bien. Sin embargo, cuando se utiliza Google Analytics, esto causa grandes problemas porque el árbitro original se pierde, por lo que Google no sabe de dónde viene. Esto pierde información útil, como los términos de cualquier motor de búsqueda.

Como nota al margen, este método tiene la ventaja de eliminar cualquier parámetro que pueda haber provenido de campañas, pero aún así me permite capturarlos del lado del servidor. Dejarlos en la cadena de consulta lleva a las personas a marcadores o twitter o blog un enlace que no deberían. He visto esto varias veces donde las personas han twitteado enlaces a nuestro sitio que contienen ID de campaña.

De todos modos, estoy escribiendo un controlador 'gateway' para todas las visitas entrantes al sitio que puedo redirigir a diferentes lugares o versiones alternativas.

Por ahora me importa más acerca de Google por ahora (que los marcadores accidentales), y quiero poder enviar a alguien que visite /la página que obtendrían si fueran /home/7, que es la versión 7 de una página de inicio.

Como dije antes. Si hago esto, pierdo la capacidad de Google para analizar el referente:

 return RedirectToAction(new { controller = "home", version = 7 });

Lo que realmente quiero es un

 return ServerTransferAction(new { controller = "home", version = 7 });

lo que me dará esa vista sin una redirección del lado del cliente. Sin embargo, no creo que tal cosa exista.

Actualmente, lo mejor que se me ocurre es duplicar toda la lógica del controlador HomeController.Index(..)en mi GatewayController.IndexAcción. Esto significa que tenía que mover 'Views/Home'en 'Shared'lo que era accesible. ¿¿Debe haber una mejor manera??..

Simon_Weaver
fuente
¿Qué es exactamente lo ServerTransferActionque intentabas replicar? ¿Eso es algo real? (no pude encontrar ninguna información al respecto ... gracias por la pregunta, por cierto, la respuesta a continuación es excelente)
jleach
Busque Server.Transfer (...). Es una forma de hacer básicamente una 'redirección' en el lado del servidor donde el cliente recibe la página redirigida sin una redirección del lado del cliente. Generalmente no se recomienda con enrutamiento moderno.
Simon_Weaver
1
"Transferir" es una característica ASP.NET anticuada que ya no es necesaria en MVC debido a la capacidad de ir directamente a la acción correcta del controlador mediante el enrutamiento. Vea esta respuesta para más detalles.
NightOwl888
@ NightOwl888 sí definitivamente, pero también a veces debido a la lógica de negocios es necesario / más fácil. Miré hacia atrás para ver dónde había terminado usando esto (afortunadamente, solo estaba en un lugar), donde tengo una página de inicio que quería ser dinámica para ciertas condiciones complejas, por lo que detrás de escena muestra una ruta diferente. Definitivamente quiero evitarlo tanto como sea posible a favor de la ruta o las condiciones de la ruta, pero a veces una simple ifdeclaración es una solución demasiado tentadora.
Simon_Weaver
@Simon_Weaver - ¿Y qué hay de malo en subclasificar RouteBasepara que puedas poner tu ifdeclaración allí en lugar de doblar todo hacia atrás para saltar de un controlador a otro?
NightOwl888

Respuestas:

130

¿Qué tal una clase TransferResult? (basado en la respuesta de Stans )

/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : ActionResult
{
    public string Url { get; private set; }

    public TransferResult(string url)
    {
        this.Url = url;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        var httpContext = HttpContext.Current;

        // MVC 3 running on IIS 7+
        if (HttpRuntime.UsingIntegratedPipeline)
        {
            httpContext.Server.TransferRequest(this.Url, true);
        }
        else
        {
            // Pre MVC 3
            httpContext.RewritePath(this.Url, false);

            IHttpHandler httpHandler = new MvcHttpHandler();
            httpHandler.ProcessRequest(httpContext);
        }
    }
}

Actualizado: ahora funciona con MVC3 (usando el código de la publicación de Simon ). se debe (no haber sido capaz de probarlo) también trabajan en MVC2 al observar si o no se está ejecutando dentro de la canalización integrada de IIS 7 +.

Para total transparencia; En nuestro entorno de producción, nunca hemos usado TransferResult directamente. Utilizamos un TransferToRouteResult que a su vez las llamadas ejecutan TransferResult. Esto es lo que realmente se está ejecutando en mis servidores de producción.

public class TransferToRouteResult : ActionResult
{
    public string RouteName { get;set; }
    public RouteValueDictionary RouteValues { get; set; }

    public TransferToRouteResult(RouteValueDictionary routeValues)
        : this(null, routeValues)
    {
    }

    public TransferToRouteResult(string routeName, RouteValueDictionary routeValues)
    {
        this.RouteName = routeName ?? string.Empty;
        this.RouteValues = routeValues ?? new RouteValueDictionary();
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        var urlHelper = new UrlHelper(context.RequestContext);
        var url = urlHelper.RouteUrl(this.RouteName, this.RouteValues);

        var actualResult = new TransferResult(url);
        actualResult.ExecuteResult(context);
    }
}

Y si estás usando T4MVC (si no ... ¡hazlo!), Esta extensión puede ser útil.

public static class ControllerExtensions
{
    public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
    {
        return new TransferToRouteResult(result.GetRouteValueDictionary());
    }
}

Usando esta pequeña gema puedes hacer

// in an action method
TransferToAction(MVC.Error.Index());
Markus Olsson
fuente
1
Esto funciona muy bien. tenga cuidado de no terminar con un bucle infinito, como hice en mi primer intento al pasar la URL incorrecta. Hice una pequeña modificación para permitir que se pase una colección de valores de ruta que puede ser útil para otros. publicado arriba o abajo ...
Simon_Weaver
actualización: esta solución parece funcionar bien, y aunque la estoy usando solo en una capacidad muy limitada, todavía no he encontrado ningún problema
Simon_Weaver
Un problema: no se puede redirigir de la solicitud POST a GET, pero eso no es necesariamente algo malo. Sin embargo
hay
2
@BradLaney: puede eliminar las líneas 'var urlHelper ...' y 'var url ...' y reemplazar 'url' por 'this.Url' para el resto y funciona. :)
Michael Ulmann
1
1: acoplamiento / pruebas unitarias / compatibilidad futura. 2: mvc core / mvc samples nunca usa este singleton. 3: este singleton no está disponible en un subproceso (nulo), ya sea un subproceso de grupo o un delegado asíncrono llamado en un contexto distinto al predeterminado, como cuando se utilizan métodos de acción asíncronos. 4: solo para fines de compatibilidad, mvc establece este valor singleton en context.HttpContext antes de ingresar el código de usuario.
Softlion
47

Editar: actualizado para ser compatible con ASP.NET MVC 3

Siempre que esté usando IIS7, la siguiente modificación parece funcionar para ASP.NET MVC 3. Gracias a @nitin y @andy por señalar que el código original no funcionó.

Edición 4/11/2011: TempData rompe con Server.TransferRequest a partir de MVC 3 RTM

Modificó el código a continuación para lanzar una excepción, pero no hay otra solución en este momento.


Aquí está mi modificación basada en la versión modificada de Markus de la publicación original de Stan. Agregué un constructor adicional para tomar un diccionario de valor de ruta y le cambié el nombre a MVCTransferResult para evitar la confusión de que podría ser solo una redirección.

Ahora puedo hacer lo siguiente para una redirección:

return new MVCTransferResult(new {controller = "home", action = "something" });

Mi clase modificada:

public class MVCTransferResult : RedirectResult
{
    public MVCTransferResult(string url)
        : base(url)
    {
    }

    public MVCTransferResult(object routeValues):base(GetRouteURL(routeValues))
    {
    }

    private static string GetRouteURL(object routeValues)
    {
        UrlHelper url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()), RouteTable.Routes);
        return url.RouteUrl(routeValues);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var httpContext = HttpContext.Current;

        // ASP.NET MVC 3.0
        if (context.Controller.TempData != null && 
            context.Controller.TempData.Count() > 0)
        {
            throw new ApplicationException("TempData won't work with Server.TransferRequest!");
        }

        httpContext.Server.TransferRequest(Url, true); // change to false to pass query string parameters if you have already processed them

        // ASP.NET MVC 2.0
        //httpContext.RewritePath(Url, false);
        //IHttpHandler httpHandler = new MvcHttpHandler();
        //httpHandler.ProcessRequest(HttpContext.Current);
    }
}
Simon_Weaver
fuente
1
Esto parece no estar funcionando en MVC 3 RC. Falla en HttpHandler.ProcessRequest (), dice: 'HttpContext.SetSessionStateBehavior' solo se puede invocar antes de que se genere el evento 'HttpApplication.AcquireRequestState'.
Andy
Todavía no he tenido un cambio para mirar MVC3. avíseme si encuentra una solución
Simon_Weaver
¿Server.TransferRquest, como lo sugiere Nitin, hace lo que está intentando hacer lo anterior?
Old Geezer el
¿Por qué necesitamos verificar TempData para nulo y contar> 0?
yurart
No lo hace, pero es solo una característica de seguridad, por lo que si ya lo está usando y confiando en él, no se quedará rascándose la cabeza si desaparece
Simon_Weaver
14

Puede usar Server.TransferRequest en IIS7 + en su lugar.

Nitin Agarwal
fuente
12

Descubrí recientemente que ASP.NET MVC no es compatible con Server.Transfer (), así que he creado un método de código auxiliar (inspirado en Default.aspx.cs).

    private void Transfer(string url)
    {
        // Create URI builder
        var uriBuilder = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, Request.ApplicationPath);
        // Add destination URI
        uriBuilder.Path += url;
        // Because UriBuilder escapes URI decode before passing as an argument
        string path = Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
        // Rewrite path
        HttpContext.Current.RewritePath(path, false);
        IHttpHandler httpHandler = new MvcHttpHandler();
        // Process request
        httpHandler.ProcessRequest(HttpContext.Current);
    }

fuente
9

¿No podría simplemente crear una instancia del controlador al que le gustaría redirigir, invocar el método de acción que desea y luego devolver el resultado? Algo como:

 HomeController controller = new HomeController();
 return controller.Index();
Brian Sullivan
fuente
44
No, el controlador que cree no tendrá elementos como la configuración de Solicitud y Respuesta correctamente. Eso puede llevar a problemas.
Jeff Walker Code Ranger
Estoy de acuerdo con @JeffWalkerCodeRanger: lo mismo también después de configurar la propiedadotherController.ControllerContext = this.ControllerContext;
T-moty
7

Quería redirigir la solicitud actual a otro controlador / acción, manteniendo la ruta de ejecución exactamente igual que si se solicitara ese segundo controlador / acción. En mi caso, Server.Request no funcionaría porque quería agregar más datos. En realidad, esto es equivalente al controlador actual que ejecuta otro HTTP GET / POST y luego transmite los resultados al cliente. Estoy seguro de que habrá mejores formas de lograr esto, pero esto es lo que funciona para mí:

RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Public");
routeData.Values.Add("action", "ErrorInternal");
routeData.Values.Add("Exception", filterContext.Exception);

var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var request = new RequestContext(context, routeData);

IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(filterContext.RequestContext, "Public");
controller.Execute(request);

Su suposición es correcta: puse este código en

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

y lo estoy usando para mostrar errores a los desarrolladores, mientras que usará una redirección regular en producción. Tenga en cuenta que no quería usar la sesión ASP.NET, la base de datos u otras formas de pasar datos de excepción entre solicitudes.


fuente
7

En lugar de simular una transferencia de servidor, MVC todavía es capaz de hacer un servidor .

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
AaronLS
fuente
Siéntase libre de agregar un texto a su respuesta para explicarlo más.
Wladimir Palant
Tenga en cuenta que esto requiere MVCv3 y superior.
Seph
5

Simplemente instale el otro controlador y ejecute su método de acción.

Richard Szalay
fuente
Esto no mostrará la URL deseada en la barra de direcciones
arserbin3
@ arserbin3: tampoco lo hará Server.Transfer. Presumiblemente, este requisito es por qué la pregunta original fue publicada.
Richard Szalay
2

Puede renovar el otro controlador e invocar el método de acción que devuelve el resultado. Sin embargo, esto requerirá que coloque su vista en la carpeta compartida.

No estoy seguro de si esto es lo que querías decir con duplicado, pero:

return new HomeController().Index();

Editar

Otra opción podría ser crear su propia ControllerFactory, de esta manera puede determinar qué controlador crear.

JoshBerke
fuente
Este podría ser el enfoque, pero no parece tener el contexto correcto, incluso si digo hc.ControllerContext = this.ControllerContext. Además, busca la vista en ~ / Views / Gateway / 5.aspx y no la encuentra.
Simon_Weaver
Además, pierdes todos los filtros de acción. Probablemente quiera intentar usar el método Execute en la interfaz IController que sus controladores deben implementar. Por ejemplo: ((IController) new HomeController ()) .Ejecute (...). De esa manera, aún participas en la línea de Action Invoker. Sin embargo, tendría que averiguar exactamente qué pasar para ejecutar ... Reflector podría ayudar allí :)
Andrew Stanton-Nurse
Sí, no me gusta la idea de actualizar un controlador, creo que es mejor definir su propia fábrica de controladores, que parece ser el punto de extensión adecuado para esto. Pero apenas he arañado la superficie de este marco, así que podría estar muy lejos.
JoshBerke
1

¿El enrutamiento no solo se ocupa de este escenario por ti? es decir, para el escenario descrito anteriormente, podría crear un controlador de ruta que implementara esta lógica.

Ricardo
fuente
Se basa en condiciones programáticas. es decir, campaña 100 podría ir a ver 7 y campaña 200 podría ir a la vista 8 etc. etc. demasiado complicado para el encaminamiento
Simon_Weaver
44
¿Por qué es eso demasiado complicado para el enrutamiento? ¿Qué hay de malo con las restricciones de ruta personalizadas? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer
1

Para cualquiera que use enrutamiento basado en expresiones, usando solo la clase TransferResult anterior, aquí hay un método de extensión de controlador que hace el truco y preserva TempData. No hay necesidad de TransferToRouteResult.

public static ActionResult TransferRequest<T>(this Controller controller, Expression<Action<T>> action)
    where T : Controller
{
     controller.TempData.Keep();
     controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
     var url = LinkBuilder.BuildUrlFromExpression(controller.Request.RequestContext, RouteTable.Routes, action);
     return new TransferResult(url);
}
Stephane Legay
fuente
Advertencia: esto parece causar un error 'La clase SessionStateTempDataProvider requiere que el estado de la sesión esté habilitado' aunque en realidad todavía funciona. Solo veo este error en mis registros. Estoy usando ELMAH para el registro de errores y obtengo este error para InProc y AppFabric
Simon_Weaver
1

Server.TransferRequest es completamente innecesario en MVC . Esta es una característica anticuada que solo era necesaria en ASP.NET porque la solicitud llegó directamente a una página y tenía que haber una manera de transferir una solicitud a otra página. Las versiones modernas de ASP.NET (incluido MVC) tienen una infraestructura de enrutamiento que se puede personalizar para enrutar directamente al recurso que se desea. No tiene sentido dejar que la solicitud llegue a un controlador solo para transferirla a otro controlador cuando simplemente puede hacer que la solicitud vaya directamente al controlador y la acción que desee.

Lo que es más, es que ya estás respondiendo a la solicitud original , no hay necesidad de meter nada enTempData u otro almacenamiento solo por el hecho de enrutar la solicitud al lugar correcto. En cambio, llega a la acción del controlador con la solicitud original intacta. También puede estar seguro de que Google aprobará este enfoque, ya que ocurre completamente en el lado del servidor.

Si bien puedes hacer un poco de ambos IRouteConstraintyIRouteHandler , el punto de extensión más poderoso para el enrutamiento es la RouteBasesubclase. Esta clase se puede ampliar para proporcionar rutas entrantes y generación de URL salientes, lo que lo convierte en una ventanilla única para todo lo que tenga que ver con la URL y la acción que ejecuta la URL.

Entonces, para seguir su segundo ejemplo, para llegar /a /home/7, simplemente necesita una ruta que agregue los valores de ruta apropiados.

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        // Routes directy to `/home/7`
        routes.MapRoute(
            name: "Home7",
            url: "",
            defaults: new { controller = "Home", action = "Index", version = 7 }
        );

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Pero volviendo a su ejemplo original donde tiene una página aleatoria, es más complejo porque los parámetros de ruta no pueden cambiar en tiempo de ejecución. Por lo tanto, podría hacerse con una RouteBasesubclase de la siguiente manera.

public class RandomHomePageRoute : RouteBase
{
    private Random random = new Random();

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData result = null;

        // Only handle the home page route
        if (httpContext.Request.Path == "/")
        {
            result = new RouteData(this, new MvcRouteHandler());

            result.Values["controller"] = "Home";
            result.Values["action"] = "Index";
            result.Values["version"] = random.Next(10) + 1; // Picks a random number from 1 to 10
        }

        // If this isn't the home page route, this should return null
        // which instructs routing to try the next route in the route table.
        return result;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        var controller = Convert.ToString(values["controller"]);
        var action = Convert.ToString(values["action"]);

        if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
            action.Equals("Index", StringComparison.OrdinalIgnoreCase))
        {
            // Route to the Home page URL
            return new VirtualPathData(this, "");
        }

        return null;
    }
}

Que se puede registrar en rutas como:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        // Routes to /home/{version} where version is randomly from 1-10
        routes.Add(new RandomHomePageRoute());

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Tenga en cuenta que en el ejemplo anterior, podría tener sentido almacenar también una cookie que registre la versión de la página de inicio en la que entró el usuario para que cuando regrese reciba la misma versión de la página de inicio.

Tenga en cuenta también que con este enfoque puede personalizar el enrutamiento para tener en cuenta los parámetros de la cadena de consulta (los ignora por completo de forma predeterminada) y enrutarlos a una acción de controlador adecuada en consecuencia.

Ejemplos adicionales

NightOwl888
fuente
¿Qué sucede si no quiero transferir inmediatamente al ingresar a una acción, sino dejar que esa acción haga algún trabajo y luego transferir condicionalmente a otra acción? Cambiar mi ruta para ir directamente al objetivo de transferencia no funcionará, por lo que parece que Server.TransferRequest, después de todo, no es "completamente innecesario en MVC".
ProfK
0

No es una respuesta per se, pero claramente el requisito sería no solo para que la navegación real "haga" la funcionalidad equivalente de Webforms Server.Transfer (), sino también que todo esto sea totalmente compatible con las pruebas unitarias.

Por lo tanto, ServerTransferResult debería "verse" como un RedirectToRouteResult y ser lo más similar posible en términos de la jerarquía de clases.

Estoy pensando en hacer esto mirando Reflector, y haciendo cualquier clase de RedirectToRouteResult y también los diversos métodos de la clase base del Controlador, y luego "agregando" este último al Controlador a través de métodos de extensión. ¿Quizás estos podrían ser métodos estáticos dentro de la misma clase, para facilitar / hacer más lenta la descarga?

Si llego a hacerlo, lo publicaré, de lo contrario, ¡tal vez alguien más podría ganarme!

Guillermo
fuente
0

Logré esto aprovechando el Html.RenderActionayudante en una Vista:

@{
    string action = ViewBag.ActionName;
    string controller = ViewBag.ControllerName;
    object routeValues = ViewBag.RouteValues;
    Html.RenderAction(action, controller, routeValues);
}

Y en mi controlador:

public ActionResult MyAction(....)
{
    var routeValues = HttpContext.Request.RequestContext.RouteData.Values;    
    ViewBag.ActionName = "myaction";
    ViewBag.ControllerName = "mycontroller";
    ViewBag.RouteValues = routeValues;    
    return PartialView("_AjaxRedirect");
}
Colin
fuente