web-dev-qa-db-fra.com

Comment simuler Server.Transfer dans ASP.NET MVC?

Dans ASP.NET MVC, vous pouvez très facilement renvoyer un ActionResult de redirection:

 return RedirectToAction("Index");

 or

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

Cela donnera effectivement une redirection HTTP, ce qui est normalement correct. Toutefois, l'utilisation de Google Analytics entraîne de gros problèmes, car le référent d'origine est perdu. Google ne sait pas d'où vous venez. Cela perd des informations utiles telles que les termes des moteurs de recherche.

En remarque, cette méthode présente l'avantage de supprimer tous les paramètres pouvant provenir de campagnes, mais me permet néanmoins de les capturer côté serveur. Si vous les laissez dans la chaîne de requête, cela amènera les gens à mettre en favori, Twitter ou bloguer, un lien qu'ils ne devraient pas. J'ai vu cela à plusieurs reprises, lorsque des personnes ont twitter des liens vers notre site contenant des ID 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 des endroits différents ou des versions alternatives. 

Pour l'instant, je me soucie davantage de Google pour le moment (que de la mise en favori accidentelle) et je veux pouvoir envoyer une personne qui visite / à la page qu'elle obtiendrait si elle accédait à /home/7, qui est la version 7 d'une page d'accueil.

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

 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 une redirection côté client. Je ne pense pas qu'une telle chose existe cependant.

Actuellement, la meilleure chose à faire est de dupliquer toute la logique du contrôleur pour HomeController.Index(..) dans mon Action GatewayController.Index. Cela signifie que je devais déplacer 'Views/Home' dans 'Shared' pour qu'il soit accessible. Il doit y avoir un meilleur moyen ?? ..

120
Simon_Weaver

Que diriez-vous d'une classe TransferResult? (basé sur Stans answer )

/// <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 le post de Simon ). Il devrait (n’a pas pu le tester) fonctionne également dans MVC2 en vérifiant s’il fonctionne 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 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 que vous pouvez faire

// in an action method
TransferToAction(MVC.Error.Index());
129
Markus Olsson

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 pour 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 générer une exception - mais aucune autre solution pour le moment.


Voici ma modification basée sur la version modifiée de Markus du message original de Stan. J'ai ajouté un constructeur supplémentaire pour prendre un dictionnaire Route Value - 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);
    }
}
48
Simon_Weaver

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

13
Nitin Agarwal

J'ai récemment découvert 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);
    }
12
Stan

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

 HomeController controller = new HomeController();
 return controller.Index();
9
Brian Sullivan

Je souhaitais réacheminer la demande actuelle vers un autre contrôleur/action, tout en conservant le chemin d'exécution exactement le même que si ce second contrôleur/action était demandé. Dans mon cas, Server.Request ne fonctionnerait pas car je voulais ajouter plus de données. Cela équivaut en réalité au gestionnaire actuel exécutant un autre HTTP GET/POST, puis transmettant les résultats au client. Je suis sûr qu'il y aura de meilleurs moyens 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);

Vous avez raison: je mets ce code dans 

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

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

7
user191966

Plutôt que de simuler un transfert de serveur, MVC est toujours capable de réellement effectuer un Server.TransferRequest :

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
6
AaronLS

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

5
Richard Szalay

Vous pouvez créer un nouveau contrôleur et invoquer la méthode d'action qui renvoie le résultat. Cela nécessitera cependant que vous placiez votre vue dans le dossier partagé.

Je ne sais pas si c'est ce que vous vouliez dire par duplicata mais:

return new HomeController().Index();

Modifier

Une autre option pourrait être de créer votre propre ControllerFactory. Vous pourrez ainsi déterminer quel contrôleur créer. 

2
JoshBerke

Pour ceux qui utilisent le routage basé sur des expressions, en 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);
}
1
Stephane Legay

Le routage ne prend-il pas en charge 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.

1
Richard

Server.TransferRequest est 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 venait 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) disposent d'une infrastructure de routage pouvant être personnalisée pour acheminer directement vers la ressource souhaitée. Il est inutile de laisser la demande atteindre un contrôleur uniquement pour la transférer à un autre contrôleur lorsque vous pouvez simplement faire en sorte que la demande soit directement transmise au contrôleur et à l'action souhaitée.

De plus, étant donné que vous répondez à la demande originale, il n’est pas nécessaire de ranger quoi que ce soit dans TempData ou dans un autre stockage simplement pour acheminer la demande au bon endroit. Au lieu de cela, vous arrivez à l'action du contrôleur avec la requête d'origine intacte. Vous pouvez également être assuré que Google approuvera cette approche, car elle se déroule entièrement côté serveur.

Bien que vous puissiez faire un peu à la fois avec IRouteConstraint et IRouteHandler, le point d’extension le plus puissant pour le routage est la sous-classe RouteBase. Cette classe peut être étendue pour fournir à la fois des itinéraires entrants et une génération d'URL sortante, ce qui en fait un guichet unique pour tout ce qui concerne l'URL et l'action qu'elle exécute.

Donc, pour suivre votre deuxième exemple, pour passer de / à /home/7, vous avez simplement besoin d’une route qui ajoute les valeurs de route 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 revenons à votre exemple initial où vous avez une page aléatoire, elle est plus complexe car les paramètres de route ne peuvent pas changer à l'exécution. Donc, cela pourrait être fait avec une sous-classe RouteBase 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 }
        );
    }
}

Remarque 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é afin qu'il reçoive la même version de la page d'accueil lors de son retour.

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

Exemples supplémentaires

1
NightOwl888

J'ai atteint cet objectif en exploitant l'assistant Html.RenderAction 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");
}
0
Colin

Ce n'est pas une réponse en soi, mais il est clair que l'exigence ne consiste pas uniquement en une navigation permettant de "reproduire" la fonctionnalité équivalente de Webforms Server.Transfer (), mais également à ce que tout cela soit pleinement pris en charge dans les tests unitaires.

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

Je pense faire cela en regardant Reflector, en faisant n'importe quelle classe RedirectToRouteResult et les différentes méthodes de la classe de base du contrôleur, puis en "ajoutant" cette dernière au contrôleur via des méthodes d'extension. Peut-être que cela pourrait être des méthodes statiques au sein de la même classe, pour la facilité/la paresse du téléchargement?

Si je réussis à le faire, je le posterai, sinon quelqu'un d'autre pourrait me battre!

0
William