Come simulare Server.Transfer in ASP.NET MVC?


124

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

 return RedirectToAction("Index");

 or

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

Questo effettivamente fornirà un reindirizzamento HTTP, che normalmente va bene. Tuttavia, quando si utilizza Google Analytics, ciò causa grossi problemi perché il referer originale viene perso, quindi Google non sa da dove vieni. Ciò perde informazioni utili come qualsiasi termine del 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 ancora di acquisirli sul lato server. Lasciandoli nella stringa di query porta le persone ai segnalibri o Twitter o blog un link che non dovrebbero. L'ho visto diverse volte in cui le persone hanno twitterato link al nostro sito contenenti ID campagna.

In ogni caso, sto scrivendo un controller "gateway" per tutte le visite in arrivo sul sito che posso reindirizzare verso luoghi diversi o versioni alternative.

Per ora mi interessa di più di Google per ora (piuttosto che dei segnalibri accidentali) e voglio essere in grado di inviare qualcuno che visita /la pagina che otterrebbe se fosse andato a /home/7, che è la versione 7 di una homepage.

Come ho detto prima, se lo faccio perdo la capacità di google di analizzare il referer:

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

Quello che voglio davvero è un

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

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

Attualmente la cosa migliore che posso inventare è duplicare l'intera logica del controller HomeController.Index(..)nella mia GatewayController.Indexazione. Ciò significa che ho dovuto trasferirmi 'Views/Home'in 'Shared'modo che fosse accessibile. Deve esserci un modo migliore ?? ..


Cos'è esattamente ServerTransferActionquello che stavi cercando di replicare? È una cosa reale? (non sono riuscito a trovare alcuna informazione al riguardo ... grazie per la domanda, tra l'altro, la risposta qui sotto è superba)
jleach

Cerca Server.Transfer (...). È sostanzialmente un modo per eseguire un "reindirizzamento" sul lato server in cui il client riceve la pagina reindirizzata senza un reindirizzamento lato client. Generalmente non è raccomandato con il routing moderno.
Simon_Weaver,

1
"Trasferimento" è una funzionalità ASP.NET antiquata che non è più necessaria in MVC a causa della capacità di passare direttamente all'azione del controller corretta utilizzando il routing. Vedi questa risposta per i dettagli.
NightOwl888,

@ NightOwl888 sì sicuramente - ma a volte anche a causa della logica aziendale è necessario / più semplice. Ho guardato indietro per vedere dove avevo finito per usarlo - (fortunatamente era solo in un posto) - dove ho una homepage che volevo essere dinamica per certe condizioni complesse e quindi dietro le quinte mostra un percorso diverso. Sicuramente voglio evitarlo il più possibile a favore del routing o delle condizioni del percorso, ma a volte una semplice ifaffermazione è una soluzione troppo allettante.
Simon_Weaver,

@Simon_Weaver - E cosa c'è che non va nella sottoclasse in RouteBasemodo da poter inserire la tua ifdichiarazione lì invece di piegare tutto all'indietro per saltare da un controller a un altro?
NightOwl888,

Risposte:


130

Che ne dici di una classe TransferResult? (basato sulla risposta di 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);
        }
    }
}

Aggiornato: ora funziona con MVC3 (usando il codice dal post di Simon ). Si dovrebbe (non sono stati in grado di provarlo) anche il lavoro in MVC2, cercando in se o non è in esecuzione all'interno della pipeline integrata di IIS7 +.

Per la massima trasparenza; Nel nostro ambiente di produzione non abbiamo mai usato direttamente TransferResult. Usiamo TransferToRouteResult che a sua volta chiama esegue TransferResult. Ecco cosa è effettivamente in esecuzione sui 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 (in caso contrario ... fallo!) Questa estensione potrebbe tornare utile.

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

Usando questo piccolo gioiello puoi farlo

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

1
funziona alla grande. fai attenzione a non finire con un ciclo infinito - come ho fatto al mio primo tentativo passando l'URL sbagliato. Ho fatto una piccola modifica per consentire il passaggio di una raccolta di valori di percorso che può essere utile ad altri. pubblicato sopra o sotto ...
Simon_Weaver il

aggiornamento: questa soluzione sembra funzionare bene e anche se la sto usando solo a capacità molto limitata non ho ancora riscontrato alcun problema
Simon_Weaver

un problema: non è possibile reindirizzare dalla richiesta POST a GET, ma non è necessariamente una cosa negativa. qualcosa di cui fare attenzione
Simon_Weaver,

2
@BradLaney: puoi semplicemente rimuovere le righe 'var urlHelper ...' e 'var url ...' e sostituire 'url' con 'this.Url' per il resto e funziona. :)
Michael Ulmann,

1
1: accoppiamento / test unitario / compatibilità futura. 2: i campioni mvc core / mvc non usano mai questo singleton. 3: questo singleton non è disponibile in un thread (null), in un thread pool o in un delegato asincrono chiamato in un contesto diverso da quello predefinito, come quando si utilizzano metodi di azione asincroni. 4: solo a fini di compatibilità, mvc imposta questo valore singleton su context.HttpContext prima di inserire il codice utente.
Softlion,

47

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 aver sottolineato il codice originale non ha funzionato.

Modifica 4/11/2011: TempData si rompe con Server.TransferRequest a partire da MVC 3 RTM

Modificato il codice seguente per generare un'eccezione, ma al momento non esiste altra soluzione.


Ecco la mia modifica basata sulla versione modificata di Markus del post originale di Stan. Ho aggiunto un costruttore aggiuntivo per prendere un dizionario del valore di route e l'ho rinominato MVCTransferResult per evitare confusione sul fatto che potrebbe essere solo un reindirizzamento.

Ora posso fare quanto segue per un reindirizzamento:

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

La mia classe 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);
    }
}

1
Questo sembra non funzionare in MVC 3 RC. Non riesce su HttpHandler.ProcessRequest (), dice: 'HttpContext.SetSessionStateBehavior' può essere invocato solo prima che venga generato l'evento 'HttpApplication.AcquireRequestState'.
Andy,

non ho ancora avuto un cambiamento per guardare MVC3. fatemi sapere se trovate una soluzione
Simon_Weaver

Server.TransferRquest come suggerito da Nitin fa ciò che sta cercando di fare?
Old Geezer,

Perché dobbiamo controllare TempData per null e contare> 0?
yurart,

Non lo fai, ma è solo una funzione di sicurezza, quindi se lo stai già utilizzando e facendo affidamento su di esso, non ti
lascerai grattarti


12

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

9

Non potresti semplicemente creare un'istanza del controller a cui desideri reindirizzare, invocare il metodo di azione desiderato, quindi restituire il risultato? Qualcosa di simile a:

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

4
No, il controller che crei non avrà impostazioni come richiesta e risposta configurate correttamente su di esso. Ciò può causare problemi.
Jeff Walker Code Ranger,

Sono d'accordo con @JeffWalkerCodeRanger: la stessa cosa anche dopo aver impostato la proprietàotherController.ControllerContext = this.ControllerContext;
T-moty

7

Volevo reindirizzare la richiesta corrente a un altro controller / azione, mantenendo lo stesso percorso di esecuzione esattamente come se fosse richiesto quel secondo controller / azione. Nel mio caso, Server.Request non funzionava perché volevo aggiungere più dati. Questo in realtà equivale all'attuale gestore che esegue un altro HTTP GET / POST, quindi trasmette i risultati al client. Sono sicuro che ci saranno modi migliori per raggiungere questo obiettivo, 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 è corretta: ho inserito questo codice

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

e lo sto usando per mostrare errori agli sviluppatori, mentre utilizzerà 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.


7

Invece di simulare un trasferimento del 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
}

Sentiti libero di aggiungere del testo alla tua risposta per spiegarlo ulteriormente.
Wladimir Palant,

Nota che questo richiede MVCv3 e versioni successive.
Seph,

5

Basta istanziare l'altro controller ed eseguire il suo metodo di azione.


Questo non mostrerà l'URL desiderato nella barra degli indirizzi
arserbin3

@ arserbin3 - Nemmeno Server.Transfer. Questo requisito è presumibilmente il motivo per cui la domanda originale è stata pubblicata.
Richard Szalay,

2

È possibile riavviare l'altro controller e richiamare il metodo di azione che restituisce il risultato. Ciò richiederà tuttavia 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 quella di creare la propria ControllerFactory, in questo modo è possibile determinare quale controller creare.


questo potrebbe essere l'approccio, ma non sembra avere il contesto giusto, anche se dico hc.ControllerContext = this.ControllerContext. Inoltre cerca la vista in ~ / Views / Gateway / 5.aspx e non la trova.
Simon_Weaver,

Inoltre perdi tutti i filtri di azione. Probabilmente vuoi provare a usare il metodo Execute sull'interfaccia IController che i tuoi controller devono implementare. Ad esempio: ((IController) new HomeController ()). Eseguire (...). In questo modo partecipi ancora alla pipeline di Action Invoker. Dovresti capire esattamente cosa passare a Execute però ... Reflector potrebbe aiutarti lì :)
Andrew Stanton-Nurse

Sì, non mi piace l'idea di rinnovare un controller, penso che sia meglio definire la propria fabbrica di controller che sembra il punto di estensione appropriato per questo. Ma ho appena graffiato la superficie di questo quadro, quindi potrei essere molto lontano.
JoshBerke,

1

Il routing non si occupa solo di questo scenario per te? vale a dire per lo scenario sopra descritto, è possibile creare un gestore di route che implementa questa logica.


si basa su condizioni programmatiche. vale a dire 100 campagna potrebbe andare a vedere 7 e campagna 200 potrebbe andare per visualizzare 8 ecc ecc troppo complicato per il routing
Simon_Weaver

4
Perché è troppo complicato per il routing? Cosa c'è di sbagliato con i vincoli di percorso personalizzati? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer

1

Per chiunque utilizzi il routing basato su espressioni, usando solo la classe TransferResult sopra, ecco un metodo di estensione del controller che fa il trucco e conserva TempData. Non è necessario 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);
}

Avviso: questo sembra causare un errore "La classe SessionStateTempDataProvider richiede che lo stato della sessione sia abilitato" sebbene funzioni ancora. Vedo solo questo errore nei miei registri. Sto usando ELMAH per la registrazione degli errori e ottengo questo errore per InProc e AppFabric
Simon_Weaver,

1

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

Inoltre, poiché stai rispondendo alla richiesta originale , non è necessario inserire nulla TempDatao altro spazio di archiviazione 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é si verifica interamente sul lato server.

Sebbene sia possibile fare abbastanza da entrambi IRouteConstrainte IRouteHandler, il punto di estensione più potente per il routing è la RouteBasesottoclasse. Questa classe può essere estesa per fornire sia rotte in entrata che generazione di URL in uscita, il che la rende uno sportello unico per tutto ciò che ha a che fare con l'URL e l'azione che l'URL esegue.

Quindi, per seguire il tuo secondo esempio, per ottenere da /a /home/7, hai semplicemente bisogno di una rotta che aggiunga i valori di rotta 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 all'esempio originale in cui hai una pagina casuale, è più complesso perché i parametri del percorso non possono cambiare in fase di esecuzione. Quindi, potrebbe essere fatto con una RouteBasesottoclasse 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;
    }
}

Che 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 sopra, potrebbe avere senso memorizzare anche un cookie che registra la versione della home page su cui l'utente è entrato, quindi quando ritornano ricevono la stessa versione della home page.

Si noti inoltre che utilizzando questo approccio è possibile personalizzare il routing per prendere in considerazione i parametri della stringa di query (li ignora completamente per impostazione predefinita) e instradare di conseguenza ad un'azione del controller appropriata.

Esempi aggiuntivi


E se non volessi trasferirmi immediatamente dopo aver inserito un'azione, ma piuttosto lasciare che quell'azione faccia un po 'di lavoro e poi trasferisca in modo condizionale ad un'altra azione. Cambiare il mio percorso per passare direttamente alla destinazione del trasferimento non funzionerà, quindi sembra che Server.TransferRequestnon sia "completamente inutile in MVC".
ProfK

0

Non una risposta di per sé, ma chiaramente il requisito sarebbe non solo che la navigazione effettiva "eseguisse" le funzionalità equivalenti di Webforms Server.Transfer (), ma anche che tutto ciò fosse pienamente supportato nell'ambito del test unitario.

Pertanto ServerTransferResult dovrebbe "apparire" come un RedirectToRouteResult ed essere il più simile possibile in termini di gerarchia di classi.

Sto pensando di farlo guardando Reflector e facendo qualunque cosa RedirectToRouteResult e anche i vari metodi della classe base del Controller, e poi "aggiungendo" quest'ultimo al Controller tramite metodi di estensione. Forse questi potrebbero essere metodi statici all'interno della stessa classe, per facilità / pigrizia di download?

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


0

Ho raggiunto questo obiettivo sfruttando l' Html.RenderActionhelper 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");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.