Comment simuler Server.Transfer dans ASP.NET MVC?

124

Dans ASP.NET MVC, vous pouvez renvoyer un ActionResult de redirection assez facilement:

 return RedirectToAction("Index");

 or

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

Cela donnera en fait une redirection HTTP, ce qui est normalement bien. Cependant, lorsque vous utilisez Google Analytics, cela pose de gros problèmes car le référent d'origine est perdu et Google ne sait donc pas d'où vous venez. Cela perd des informations utiles telles que les termes des moteurs de recherche.

En passant, cette méthode a l'avantage de supprimer tous les paramètres pouvant provenir de campagnes mais me permet quand même de les capturer côté serveur. Les laisser dans la chaîne de requête amène les gens à mettre en favori ou à twitter ou à bloguer un lien qu'ils ne devraient pas. J'ai vu cela plusieurs fois où des gens ont envoyé des liens Twitter vers notre site contenant des identifiants de campagne.

Quoi qu'il en soit, j'écris un contrôleur de «passerelle» pour toutes les visites entrantes sur le site que je peux rediriger vers différents endroits ou des versions alternatives.

Pour l'instant, je me soucie plus de Google pour le moment (que de la mise en signet accidentelle), et je veux pouvoir envoyer quelqu'un qui visite /la page qu'il obtiendrait s'il allait /home/7, qui est la version 7 d'une page d'accueil.

Comme je l'ai déjà dit, si je fais cela, je perds la possibilité pour Google d'analyser le référent:

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

Ce que je veux vraiment, c'est un

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

ce qui me donnera cette vue sans redirection côté client. Je ne pense pas qu'une telle chose existe cependant.

Actuellement, la meilleure chose que je puisse proposer est de dupliquer toute la logique du contrôleur HomeController.Index(..)dans mon GatewayController.Indexaction. Cela signifie que je devais passer 'Views/Home'en 'Shared'sorte qu'il était accessible. Il doit y avoir un meilleur moyen ?? ..

Simon_Weaver
la source
Qu'est-ce ServerTransferActionque vous essayez de reproduire exactement ? Est-ce une chose réelle? (je
n'ai
Recherchez Server.Transfer (...). C'est un moyen de faire une "redirection" côté serveur où le client reçoit la page redirigée sans redirection côté client. Généralement, ce n'est pas recommandé avec le routage moderne.
Simon_Weaver
1
Le «transfert» est une fonctionnalité ASP.NET désuète qui n'est plus nécessaire dans MVC en raison de la possibilité d' accéder directement à l'action de contrôleur appropriée à l'aide du routage. Voir cette réponse pour plus de détails.
NightOwl888
@ NightOwl888 oui certainement - mais aussi parfois en raison de la logique métier, c'est nécessaire / plus facile. J'ai regardé en arrière pour voir où j'avais fini par utiliser cela - (heureusement, ce n'était qu'à un seul endroit) - où j'ai une page d'accueil que je voulais dynamique pour certaines conditions complexes et donc dans les coulisses, cela montre un itinéraire différent. Je veux vraiment l'éviter autant que possible en faveur des conditions de routage ou de route - mais parfois une simple ifdéclaration est une solution trop tentante.
Simon_Weaver
@Simon_Weaver - Et quel est le problème avec le sous-classement RouteBasepour que vous puissiez y mettre votre ifdéclaration au lieu de tout plier en arrière pour passer d'un contrôleur à un autre?
NightOwl888

Réponses:

130

Que diriez-vous d'une classe TransferResult? (basé sur la réponse 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);
        }
    }
}

Mise à jour: fonctionne maintenant avec MVC3 (en utilisant le code de l'article de Simon ). Il devrait (n'a pas pu le tester) également fonctionner dans MVC2 en vérifiant s'il s'exécute ou non dans le pipeline intégré d'IIS7 +.

Pour une transparence totale; Dans notre environnement de production, nous n'utilisons jamais directement TransferResult. Nous utilisons un TransferToRouteResult qui à son tour appelle exécute le TransferResult. Voici ce qui fonctionne réellement sur mes serveurs de production.

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);
    }
}

Et si vous utilisez T4MVC (sinon ... faites!), Cette extension peut être utile.

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

En utilisant ce petit bijou, vous pouvez faire

// in an action method
TransferToAction(MVC.Error.Index());
Markus Olsson
la source
1
cela fonctionne très bien. veillez à ne pas vous retrouver avec une boucle infinie - comme je l'ai fait lors de ma première tentative en passant la mauvaise URL. J'ai fait une petite modification pour permettre à une collection de valeurs de route d'être transmise, ce qui peut être utile à d'autres. posté ci-dessus ou ci-dessous ...
Simon_Weaver
mise à jour: cette solution semble bien fonctionner, et bien que je ne l'utilise que dans une capacité très limitée, je n'ai pas encore trouvé de problème
Simon_Weaver
un problème: impossible de rediriger de POST à ​​la demande GET - mais ce n'est pas nécessairement une mauvaise chose. quelque chose dont il faut se méfier
Simon_Weaver
2
@BradLaney: Vous pouvez simplement supprimer les lignes 'var urlHelper ...' et 'var url ...' et remplacer 'url' par 'this.Url' pour le reste et ça marche. :)
Michael Ulmann
1
1: couplage / test unitaire / compatibilité future. 2: les échantillons mvc core / mvc n'utilisent jamais ce singleton. 3: ce singleton n'est pas disponible dans un thread (nul), que ce soit un thread de pool ou un délégué async appelé sur un contexte autre que default, comme lors de l'utilisation de méthodes d'action async. 4: à des fins de compatibilité uniquement, mvc définit cette valeur singleton sur context.HttpContext avant de saisir le code utilisateur.
Softlion
47

Edit: mis à jour pour être compatible avec ASP.NET MVC 3

Si vous utilisez IIS7, la modification suivante semble fonctionner pour ASP.NET MVC 3. Merci à @nitin et @andy d'avoir signalé que le code d'origine ne fonctionnait pas.

Edit 4/11/2011: TempData rompt avec Server.TransferRequest à partir de MVC 3 RTM

Modification du code ci-dessous pour lever une exception - mais aucune autre solution pour le moment.


Voici ma modification basée sur la version modifiée par Markus du message original de Stan. J'ai ajouté un constructeur supplémentaire pour prendre un dictionnaire de valeurs de route - et je l'ai renommé MVCTransferResult pour éviter toute confusion selon laquelle il pourrait s'agir simplement d'une redirection.

Je peux maintenant faire ce qui suit pour une redirection:

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

Ma classe modifiée:

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
la source
1
Cela ne semble pas fonctionner dans MVC 3 RC. Échoue sur HttpHandler.ProcessRequest (), dit: «HttpContext.SetSessionStateBehavior» ne peut être appelé qu'avant que l'événement «HttpApplication.AcquireRequestState» ne soit déclenché.
Andy
je n'ai pas encore eu de changement pour regarder MVC3. faites-moi savoir si vous trouvez une solution
Simon_Weaver
Server.TransferRquest, comme suggéré par Nitin, fait-il ce que ce qui précède tente de faire?
Old Geezer
Pourquoi avons-nous besoin de vérifier TempData pour null et count> 0?
yurart
Vous ne le faites pas, mais c'est juste une fonction de sécurité, donc si vous l'utilisez déjà et que vous comptez dessus, vous ne vous
gratterez
14

Vous pouvez utiliser Server.TransferRequest sur IIS7 + à la place.

Nitine Agarwal
la source
12

J'ai découvert récemment qu'ASP.NET MVC ne prend pas en charge Server.Transfer (), j'ai donc créé une méthode stub (inspirée de 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);
    }

la source
9

Ne pourriez-vous pas simplement créer une instance du contrôleur vers lequel vous souhaitez rediriger, invoquer la méthode d'action souhaitée, puis renvoyer le résultat? Quelque chose comme:

 HomeController controller = new HomeController();
 return controller.Index();
Brian Sullivan
la source
4
Non, le contrôleur que vous créez n'aura pas des éléments tels que la configuration des requêtes et des réponses correctement. Cela peut entraîner des problèmes.
Jeff Walker Code Ranger
Je suis d'accord avec @JeffWalkerCodeRanger: la même chose aussi après avoir défini la propriétéotherController.ControllerContext = this.ControllerContext;
T-moty
7

Je voulais réacheminer la demande actuelle vers un autre contrôleur / action, tout en gardant le chemin d'exécution exactement le même que si ce deuxième contrôleur / action était demandé. Dans mon cas, Server.Request ne fonctionnerait pas car je voulais ajouter plus de données. C'est en fait l'équivalent du gestionnaire actuel exécutant un autre HTTP GET / POST, puis diffusant les résultats vers le client. Je suis sûr qu'il y aura de meilleures façons d'y parvenir, mais voici ce qui fonctionne pour moi:

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);

Votre hypothèse est juste: j'ai mis ce code dans

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

et je l'utilise pour afficher les erreurs aux développeurs, alors qu'il utilisera une redirection régulière en production. Notez que je ne voulais pas utiliser la session ASP.NET, la base de données ou d'autres moyens de transmettre des données d'exception entre les demandes.


la source
7

Plutôt que de simuler un transfert de serveur, MVC est toujours capable de faire un Server.TransferRequest :

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
AaronLS
la source
N'hésitez pas à ajouter du texte à votre réponse pour l'expliquer davantage.
Wladimir Palant
Notez que cela nécessite MVCv3 et supérieur.
Seph
5

Instancez simplement l'autre contrôleur et exécutez sa méthode d'action.

Richard Szalay
la source
Cela n'affichera pas l'URL souhaitée dans la barre d'adresse
arserbin3
@ arserbin3 - Server.Transfer non plus. Cette exigence explique probablement pourquoi la question initiale a même été publiée.
Richard Szalay
2

Vous pouvez mettre à jour l'autre contrôleur et appeler la méthode d'action renvoyant le résultat. Cela vous obligera cependant à placer votre vue dans le dossier partagé.

Je ne sais pas si c'est ce que vous entendez par dupliquer mais:

return new HomeController().Index();

Éditer

Une autre option pourrait être de créer votre propre ControllerFactory, de cette façon vous pouvez déterminer quel contrôleur créer.

JoshBerke
la source
c'est peut-être l'approche, mais elle ne semble pas tout à fait avoir le bon contexte - même si je dis hc.ControllerContext = this.ControllerContext. De plus, il recherche ensuite la vue sous ~ / Views / Gateway / 5.aspx et ne la trouve pas.
Simon_Weaver
De plus, vous perdez tous les filtres d'action. Vous voudrez probablement essayer d'utiliser la méthode Execute sur l'interface IController que vos contrôleurs doivent implémenter. Par exemple: ((IController) new HomeController ()). Execute (...). De cette façon, vous participez toujours au pipeline Action Invoker. Vous devrez cependant déterminer exactement ce qu'il faut passer pour Execute ... Reflector pourrait vous aider :)
Andrew Stanton-Nurse
Oui, je n'aime pas l'idée de créer un nouveau contrôleur, je pense que vous feriez mieux de définir votre propre usine de contrôleurs qui semble être le point d'extension approprié pour cela. Mais j'ai à peine effleuré la surface de ce cadre, donc je suis peut-être loin.
JoshBerke
1

Le routage ne s'occupe-t-il pas uniquement de ce scénario pour vous? c'est-à-dire que pour le scénario décrit ci-dessus, vous pouvez simplement créer un gestionnaire d'itinéraire qui implémente cette logique.

Richard
la source
son basé sur des conditions programmatiques. c'est-à-dire que la campagne 100 pourrait aller à la vue 7 et la campagne 200 pourrait aller à la vue 8, etc. etc. trop compliqué pour le routage
Simon_Weaver
4
Pourquoi est-ce trop compliqué pour le routage? Quel est le problème avec les contraintes d'itinéraire personnalisées? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer
1

Pour toute personne utilisant le routage basé sur une expression, utilisant uniquement la classe TransferResult ci-dessus, voici une méthode d'extension de contrôleur qui fait l'affaire et préserve TempData. Pas besoin 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);
}
Stéphane Legay
la source
Attention: cela semble provoquer une erreur «La classe SessionStateTempDataProvider nécessite l'activation de l'état de session» bien qu'elle fonctionne toujours. Je ne vois cette erreur que dans mes journaux. J'utilise ELMAH pour la journalisation des erreurs et j'obtiens cette erreur pour InProc et AppFabric
Simon_Weaver
1

Server.TransferRequestest complètement inutile dans MVC . Il s'agit d'une fonctionnalité obsolète qui n'était nécessaire que dans ASP.NET car la demande arrivait directement à une page et il devait y avoir un moyen de transférer une demande vers une autre page. Les versions modernes d'ASP.NET (y compris MVC) ont une infrastructure de routage qui peut être personnalisée pour acheminer directement vers la ressource souhaitée. Il ne sert à rien de laisser la demande atteindre un contrôleur uniquement pour la transférer vers un autre contrôleur lorsque vous pouvez simplement faire la demande aller directement au contrôleur et à l'action que vous souhaitez.

De plus, puisque vous répondez à la demande d' origine , il n'est pas nécessaire de rentrer quoi que ce soit dansTempData ou autre stockage juste pour le plaisir d'acheminer la demande au bon endroit. Au lieu de cela, vous arrivez à l'action du contrôleur avec la demande d'origine intacte. Vous pouvez également être assuré que Google approuvera cette approche car elle se déroule entièrement du côté serveur.

Bien que vous puissiez faire pas mal des deux IRouteConstraintet IRouteHandler, le point d'extension le plus puissant pour le routage est la RouteBasesous - classe. Cette classe peut être étendue pour fournir à la fois des routes entrantes et la génération d'URL sortantes, ce qui en fait un guichet unique pour tout ce qui a à voir avec l'URL et l'action exécutée par l'URL.

Donc, pour suivre votre deuxième exemple, pour aller de /à /home/7, vous avez simplement besoin d'un itinéraire qui ajoute les valeurs d'itinéraire appropriées.

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 }
        );
    }
}

Mais pour revenir à votre exemple d'origine où vous avez une page aléatoire, c'est plus complexe car les paramètres d'itinéraire ne peuvent pas changer au moment de l'exécution. Ainsi, cela pourrait être fait avec une RouteBasesous - classe comme suit.

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;
    }
}

Qui peut être enregistré dans le routage comme:

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 }
        );
    }
}

Notez que dans l'exemple ci-dessus, il peut être judicieux de stocker également un cookie enregistrant la version de la page d'accueil sur laquelle l'utilisateur est entré, de sorte qu'à son retour, il reçoive la même version de la page d'accueil.

Notez également qu'en utilisant cette approche, vous pouvez personnaliser le routage pour prendre en compte les paramètres de la chaîne de requête (il les ignore complètement par défaut) et acheminer vers une action de contrôleur appropriée en conséquence.

Exemples supplémentaires

NightOwl888
la source
Que faire si je ne veux pas transférer immédiatement en entrant dans une action, mais plutôt laisser cette action faire un certain travail et ensuite transférer conditionnellement à une autre action. Changer mon routage pour aller directement à la cible de transfert ne fonctionnera pas, donc il semble que ce Server.TransferRequestn'est pas, après tout, "complètement inutile dans MVC".
ProfK
0

Ce n'est pas une réponse en soi, mais il est clair que l'exigence serait non seulement que la navigation réelle "fasse" la fonctionnalité équivalente de Webforms Server.Transfer (), mais aussi que tout cela soit entièrement pris en charge dans les tests unitaires.

Par conséquent, le ServerTransferResult doit «ressembler» à un RedirectToRouteResult et être aussi similaire que possible en termes de hiérarchie de classes.

Je pense faire cela en regardant Reflector, et en faisant ce que font la classe RedirectToRouteResult et aussi les différentes méthodes de la classe de base Controller, puis en "ajoutant" ce dernier au Controller via des méthodes d'extension. Peut-être que cela pourrait être des méthodes statiques au sein de la même classe, pour faciliter / paresser le téléchargement?

Si j'arrive à le faire, je le posterai, sinon peut-être que quelqu'un d'autre pourrait me battre!

William
la source
0

J'ai réalisé cela en exploitant l' Html.RenderActionassistant dans une vue:

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

Et dans mon contrôleur:

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