Come simulare Server.Transfer in ASP.NET MVC?

In ASP.NET MVC è ansible restituire un ActionResult di reindirizzamento abbastanza facilmente:

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

Questo in realtà darà un reindirizzamento HTTP, che normalmente va bene. Tuttavia, quando si utilizza google analytics questo causa grossi problemi perché il referente originale è perso, quindi google non sa da dove vieni. Questo perde informazioni utili come i termini di qualsiasi motore di ricerca.

Come nota a margine, questo metodo ha il vantaggio di rimuovere tutti i parametri che potrebbero provenire dalle campagne, ma mi consente comunque di catturarli dal lato server. Lasciandoli nella stringa di query si arriva a persone che fanno bookmarking o twitter o blog su un link che non dovrebbero. L’ho visto più volte in cui le persone hanno twitterato collegamenti al nostro sito che contengono ID campagna.

Ad ogni modo, sto scrivendo un controller “gateway” per tutte le visite in arrivo al sito che posso redirect verso luoghi diversi o versioni alternative.

Per ora mi interessa di più di Google per ora (rispetto al bookmarking accidentale), e voglio poter inviare qualcuno che visita / alla pagina che otterrebbero se andassero a /home/7 , che è la versione 7 di una home page .

    Come ho detto prima, se lo faccio, perdo la possibilità per google di analizzare il referente:

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

    Quello che voglio veramente è un

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

    che mi otterrà quella vista senza un reindirizzamento lato client. Non penso però che esista una cosa del genere.

    Attualmente la cosa migliore che riesco a fare è duplicare l’intera logica del controller per HomeController.Index(..) nella mia azione GatewayController.Index . Ciò significa che ho dovuto spostare 'Views/Home' in 'Shared' modo che fosse accessibile. Ci deve essere un modo migliore ?? ..

    Che ne dici di una class TransferResult? (basato sulla risposta di Stans )

     ///  /// Transfers execution to the supplied url. ///  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); } } } 

    Aggiornato: ora funziona con MVC3 (usando il codice dal post di Simon ). Dovrebbe (non essere stato in grado di testarlo) funzionare anche in MVC2 osservando se è in esecuzione o meno nella pipeline integrata di IIS7 +.

    Per piena trasparenza; Nel nostro ambiente di produzione non usiamo mai TransferResult direttamente. Utilizziamo un TransferToRouteResult che a sua volta chiama esegue TransferResult. Ecco cosa succede nei miei server di produzione.

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

    E se stai usando T4MVC (se non … fai!) Questa estensione potrebbe tornare utile.

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

    Usando questa piccola gem puoi farlo

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

    Modifica: aggiornato per essere compatibile con ASP.NET MVC 3

    Se si utilizza IIS7, la seguente modifica sembra funzionare per ASP.NET MVC 3. Grazie a @nitin e @andy per indicare il codice originale non ha funzionato.

    Modifica il 4/11/2011: TempData interrompe con Server.TransferRequest da MVC 3 RTM

    Modificato il codice qui sotto per generare un’eccezione, ma al momento non ci sono altre soluzioni.


    Ecco la mia modifica basata sulla versione modificata di Markus del post originale di Stan. Ho aggiunto un costruttore aggiuntivo per prendere un dizionario Route Value e lo abbiamo rinominato MVCTransferResult per evitare confusione sul fatto che potrebbe essere solo un reindirizzamento.

    Ora posso eseguire le seguenti operazioni per un reindirizzamento:

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

    La mia class modificata:

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

    È ansible utilizzare Server.TransferRequest su IIS7 +.

    Recentemente ho scoperto che ASP.NET MVC non supporta Server.Transfer (), quindi ho creato un metodo stub (ispirato a 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); } 

    Non potresti semplicemente creare un’istanza del controller a cui desideri redirect, richiamare il metodo di azione che desideri, quindi restituire il risultato? Qualcosa di simile a:

      HomeController controller = new HomeController(); return controller.Index(); 

    Volevo redirect la richiesta corrente a un altro controller / azione, mantenendo il percorso di esecuzione esattamente come se fosse richiesto quel secondo controller / azione. Nel mio caso, Server.Request non funzionerebbe perché volevo aggiungere più dati. Questo è in realtà equivalente al gestore corrente che esegue un altro GET / POST HTTP, quindi esegue il stream dei risultati sul client. Sono sicuro che ci saranno modi migliori per raggiungere questo objective, ma ecco cosa funziona per me:

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

    La tua ipotesi è giusta: inserisco questo codice

     public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter 

    e lo sto usando per mostrare errori agli sviluppatori, mentre userà un reindirizzamento regolare in produzione. Si noti che non volevo utilizzare la sessione ASP.NET, il database o altri modi per passare i dati delle eccezioni tra le richieste.

    Anziché simulare il trasferimento di un server, MVC è ancora in grado di eseguire effettivamente un Server.TransferRequest :

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

    Basta istallare l’altro controller ed eseguire il suo metodo di azione.

    È ansible creare nuovamente l’altro controller e richiamare il metodo di azione restituendo il risultato. Ciò richiederà comunque di posizionare la vista nella cartella condivisa.

    Non sono sicuro se questo è ciò che intendevi per duplicato ma:

     return new HomeController().Index(); 

    modificare

    Un’altra opzione potrebbe essere creare il proprio ControllerFactory, in questo modo è ansible determinare quale controller creare.

    Il routing non ti prende semplicemente cura di questo scenario? vale a dire per lo scenario descritto sopra, potresti semplicemente creare un gestore di percorsi che ha implementato questa logica.

    Per chiunque usi il routing basato su espressioni, usando solo la class TransferResult sopra, ecco un metodo di estensione del controller che fa il trucco e conserva TempData. Non è necessario TransferToRouteResult.

     public static ActionResult TransferRequest(this Controller controller, Expression> 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); } 

    Server.TransferRequest è completamente necessario in MVC . Questa è una funzionalità obsoleta che era necessaria solo in ASP.NET perché la richiesta arrivava direttamente a una pagina e doveva essere un modo per trasferire una richiesta a un’altra pagina. Le versioni moderne di ASP.NET (incluso MVC) dispongono di un’infrastruttura di routing che può essere personalizzata per il routing diretto alla risorsa desiderata. Non ha senso far sì che la richiesta raggiunga un controller solo per trasferirlo su un altro controller quando puoi semplicemente fare in modo che la richiesta passi direttamente al controller e all’azione che desideri.

    Inoltre, poiché stai rispondendo alla richiesta originale , non è necessario inserire nulla in TempData o in un’altra memoria solo per il routing della richiesta nel posto giusto. Invece, si arriva all’azione del controller con la richiesta originale intatta. Puoi anche essere certo che Google approverà questo approccio poiché avviene interamente sul lato server.

    Sebbene sia ansible fare un bel po ‘da IRouteConstraint e IRouteHandler , il punto di estensione più potente per il routing è la sottoclass RouteBase . Questa class può essere estesa per fornire sia percorsi in entrata che generazione di URL in uscita, il che lo rende uno sportello unico per tutto ciò che riguarda l’URL e l’azione eseguita dall’URL.

    Quindi, per seguire il tuo secondo esempio, per andare da / a /home/7 , hai semplicemente bisogno di un percorso che aggiunga i valori di percorso appropriati.

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

    Ma tornando al tuo esempio originale in cui hai una pagina a caso, è più complesso perché i parametri del percorso non possono cambiare in fase di esecuzione. Quindi, potrebbe essere fatto con una sottoclass RouteBase come segue.

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

    Quale può essere registrato nel routing come:

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

    Nota nell’esempio precedente, potrebbe avere senso memorizzare anche un cookie che registra la versione della home page in cui l’utente è entrato, così quando ritornano ricevono la stessa versione della home page.

    Si noti inoltre che utilizzando questo approccio è ansible personalizzare il routing in modo da tenere in considerazione i parametri della stringa di query (che ignora completamente per impostazione predefinita) e indirizzare di conseguenza l’azione appropriata del controller.

    Ulteriori esempi

    Non è una risposta di per sé, ma chiaramente il requisito non sarebbe solo per la navigazione effettiva per “fare” la funzionalità equivalente di Webforms Server.Transfer (), ma anche per tutto ciò che è completamente supportato nei test delle unità.

    Pertanto, ServerTransferResult dovrebbe “apparire” come un RedirectToRouteResult e risultare il più ansible simile in termini di gerarchia di classi.

    Sto pensando di farlo guardando Reflector, e facendo qualunque sia la class RedirectToRouteResult e anche i vari metodi della class base Controller, e poi “aggiungendo” quest’ultima al Controller tramite metodi di estensione. Forse questi potrebbero essere metodi statici all’interno della stessa class, per facilità / pigrizia del download?

    Se riesco a farlo, lo posterò, altrimenti forse qualcun altro potrebbe battermi!

    Ho raggiunto questo objective sfruttando l’helper Html.RenderAction in una vista:

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

    E nel mio controller:

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