Jak symulować Server.Transfer w ASP.NET MVC?

124

W ASP.NET MVC można dość łatwo zwrócić ActionResult przekierowania:

 return RedirectToAction("Index");

 or

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

To faktycznie da przekierowanie HTTP, które zwykle jest w porządku. Jednak podczas korzystania z Google Analytics powoduje to duże problemy, ponieważ oryginalny odnośnik jest zagubiony, więc Google nie wie, skąd pochodzisz. Spowoduje to utratę przydatnych informacji, takich jak terminy wyszukiwarek.

Na marginesie, ta metoda ma tę zaletę, że usuwa wszelkie parametry, które mogły pochodzić z kampanii, ale nadal pozwala mi przechwytywać je po stronie serwera. Pozostawienie ich w ciągu zapytania powoduje, że ludzie tworzą zakładki, twitter lub blogują łącze, którego nie powinni. Widziałem to kilka razy, gdy ludzie umieszczali na Twitterze linki do naszej witryny zawierające identyfikatory kampanii.

W każdym razie piszę kontroler „bramy” dla wszystkich przychodzących wizyt w witrynie, które mogę przekierować w inne miejsca lub alternatywne wersje.

Na razie bardziej zależy mi na Google (niż na przypadkowym dodawaniu zakładek) i chcę móc wysłać kogoś, kto odwiedziłby /stronę, którą dostałby, gdyby przeszła /home/7, czyli wersję 7 strony domowej.

Tak jak powiedziałem wcześniej, jeśli to zrobię, stracę zdolność Google do analizowania osoby odsyłającej:

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

To, czego naprawdę chcę, to plik

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

co da mi ten widok bez przekierowania po stronie klienta. Nie sądzę jednak, żeby coś takiego istniało.

Obecnie najlepszą rzeczą, jaką mogę wymyślić, jest skopiowanie całej logiki kontrolera HomeController.Index(..)w mojej GatewayController.Indexakcji. Oznacza to, musiałem przenieść 'Views/Home'się 'Shared'więc było dostępne. Musi być lepszy sposób ?? ..

Simon_Weaver
źródło
Czym dokładnie jest a ServerTransferAction, które próbujesz odtworzyć? Czy to jest rzeczywista rzecz? (nie mogłem znaleźć żadnych informacji na ten temat ... dzięki za pytanie, przy okazji, odpowiedź poniżej jest
świetna
Wyszukaj Server.Transfer (...). Jest to sposób na wykonanie „przekierowania” po stronie serwera, gdzie klient otrzymuje przekierowaną stronę bez przekierowania po stronie klienta. Generalnie nie jest to zalecane w przypadku nowoczesnych routingu.
Simon_Weaver,
1
„Transfer” to przestarzała funkcja ASP.NET, która nie jest już potrzebna w MVC ze względu na możliwość bezpośredniego przejścia do właściwej akcji kontrolera przy użyciu routingu. Zobacz tę odpowiedź, aby uzyskać szczegółowe informacje.
NightOwl888
@ NightOwl888 tak, zdecydowanie - ale także czasami ze względu na logikę biznesową jest to konieczne / łatwiejsze. Spojrzałem wstecz, aby zobaczyć, gdzie skończyłem, używając tego - (na szczęście było to tylko w jednym miejscu) - gdzie mam stronę główną, którą chciałem być dynamiczna w pewnych złożonych warunkach, więc za kulisami pokazuje inną trasę. Zdecydowanie chcę tego uniknąć w jak największym stopniu na korzyść trasy lub warunków trasy - ale czasami proste ifstwierdzenie jest zbyt kuszące.
Simon_Weaver
@Simon_Weaver - A co jest złego w podklasach RouteBase, abyś mógł umieścić iftam swoje oświadczenie zamiast zginać wszystko do tyłu, aby przeskoczyć z jednego kontrolera do drugiego?
NightOwl888

Odpowiedzi:

130

A co z klasą TransferResult? (na podstawie odpowiedzi Stansa )

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

Zaktualizowano: Teraz działa z MVC3 (używając kodu z postu Simona ). to powinno (nie udało się go przetestować) również prace w MVC2 patrząc, czy nie jest uruchomiony w ramach zintegrowanego rurociągu IIS7 +.

Dla pełnej przejrzystości; W naszym środowisku produkcyjnym nigdy nie używaliśmy bezpośrednio TransferResult. Używamy TransferToRouteResult, który z kolei wywołuje wykonanie TransferResult. Oto, co faktycznie działa na moich serwerach produkcyjnych.

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

A jeśli używasz T4MVC (jeśli nie ... zrób!), To rozszerzenie może się przydać.

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

Używając tego małego klejnotu, możesz to zrobić

// in an action method
TransferToAction(MVC.Error.Index());
Markus Olsson
źródło
1
to działa świetnie. uważaj, aby nie skończyć z nieskończoną pętlą - tak jak zrobiłem to przy mojej pierwszej próbie, przekazując niewłaściwy adres URL. Dokonałem niewielkiej modyfikacji, aby umożliwić przekazanie zbioru wartości trasy, co może być przydatne dla innych. zamieszczone powyżej lub poniżej ...
Simon_Weaver
aktualizacja: to rozwiązanie wydaje się działać dobrze i chociaż używam go tylko w bardzo ograniczonym zakresie, nie znalazłem jeszcze żadnych problemów
Simon_Weaver
jeden problem: nie można przekierować z żądania POST do GET - ale niekoniecznie jest to zła rzecz. coś, na co należy jednak
uważać
2
@BradLaney: Możesz po prostu usunąć linie „var urlHelper ...” i „var url ...” i zastąpić „url” słowem „this.Url” w przypadku reszty i to działa. :)
Michael Ulmann
1
1: sprzężenie / testy jednostkowe / przyszła kompatybilność. 2: próbki mvc core / mvc nigdy nie używają tego singletona. 3: ten singleton nie jest dostępny w wątku (null), wątku puli lub delegacie asynchronicznym wywoływanym w kontekście innym niż domyślny, na przykład podczas korzystania z metod akcji asynchronicznej. 4: tylko dla celów zgodności, mvc ustawia tę pojedynczą wartość na context.HttpContext przed wprowadzeniem kodu użytkownika.
Softlion
47

Edycja: zaktualizowano w celu zapewnienia zgodności z ASP.NET MVC 3

Zakładając, że używasz IIS7, wydaje się, że następująca modyfikacja działa dla ASP.NET MVC 3. Dzięki @nitin i @andy za wskazanie, że oryginalny kod nie działa.

Edycja 11.04.2011: TempData zrywa z Server.TransferRequest od MVC 3 RTM

Zmodyfikowano poniższy kod, aby zgłosić wyjątek - ale obecnie nie ma innego rozwiązania.


Oto moja modyfikacja oparta na zmodyfikowanej wersji oryginalnego postu Stana Markusa. Dodałem dodatkowy konstruktor, aby wziąć słownik wartości trasy - i zmieniłem jego nazwę na MVCTransferResult, aby uniknąć nieporozumień, że może to być po prostu przekierowanie.

Teraz mogę wykonać następujące czynności dla przekierowania:

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

Moja zmodyfikowana klasa:

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
źródło
1
To wydaje się nie działać w MVC 3 RC. Fails on HttpHandler.ProcessRequest (), mówi: „HttpContext.SetSessionStateBehavior” można wywołać tylko przed wywołaniem zdarzenia „HttpApplication.AcquireRequestState”.
Andy
nie miałem jeszcze zmiany, aby spojrzeć na MVC3. daj mi znać, jeśli znajdziesz rozwiązanie
Simon_Weaver
Czy Server.TransferRquest zgodnie z sugestią Nitina robi to, co powyżej próbuje zrobić?
Old Geezer
Dlaczego musimy sprawdzać TempData pod kątem wartości null i count> 0?
yurart
Nie, ale to tylko funkcja bezpieczeństwa, więc jeśli już go używasz i polegasz na nim, nie będziesz musiał drapać się po głowie, jeśli zniknie
Simon_Weaver
14

Zamiast tego można użyć Server.TransferRequest w usługach IIS7 +.

Nitin Agarwal
źródło
12

Niedawno dowiedziałem się, że ASP.NET MVC nie obsługuje Server.Transfer (), więc stworzyłem metodę stub (zainspirowaną 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);
    }

źródło
9

Czy nie mógłbyś po prostu utworzyć instancji kontrolera, do którego chcesz przekierować, wywołać żądaną metodę akcji, a następnie zwrócić wynik? Coś jak:

 HomeController controller = new HomeController();
 return controller.Index();
Brian Sullivan
źródło
4
Nie, utworzony kontroler nie będzie zawierał takich elementów, jak konfiguracja żądań i odpowiedzi. To może prowadzić do problemów.
Jeff Walker Code Ranger
Zgadzam się z @JeffWalkerCodeRanger: to samo po ustawieniu właściwościotherController.ControllerContext = this.ControllerContext;
T-moty
7

Chciałem przekierować bieżące żądanie do innego kontrolera / akcji, zachowując dokładnie taką samą ścieżkę wykonania, jak w przypadku żądania tego drugiego kontrolera / akcji. W moim przypadku Server.Request nie działałby, ponieważ chciałem dodać więcej danych. W rzeczywistości jest to odpowiednik bieżącego programu obsługi wykonującego inny protokół HTTP GET / POST, a następnie przesyłającego wyniki strumieniowo do klienta. Jestem pewien, że będą lepsze sposoby na osiągnięcie tego, ale oto, co działa w moim przypadku:

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

Twoje przypuszczenie jest słuszne: umieściłem ten kod

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

i używam go do wyświetlania błędów programistom, podczas gdy w środowisku produkcyjnym będzie używać zwykłego przekierowania. Zauważ, że nie chciałem używać sesji ASP.NET, bazy danych ani innych sposobów przekazywania danych wyjątków między żądaniami.


źródło
7

Zamiast symulować transferu serwera, MVC jest nadal w stanie faktycznie robi Server.TransferRequest :

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
AaronLS
źródło
Zapraszam do dodania tekstu do swojej odpowiedzi, aby bardziej ją wyjaśnić.
Wladimir Palant
Zauważ, że wymaga to MVCv3 i nowszych.
Seph
5

Po prostu uruchom drugi kontroler i wykonaj jego metodę akcji.

Richard Szalay
źródło
To nie pokaże żądanego adresu URL w pasku adresu
arserbin3
@ arserbin3 - Ani Server.Transfer. Prawdopodobnie ten wymóg jest powodem, dla którego w ogóle opublikowano oryginalne pytanie.
Richard Szalay
2

Możesz utworzyć nowy kontroler i wywołać metodę akcji zwracającą wynik. Będzie to jednak wymagało umieszczenia widoku w folderze udostępnionym.

Nie jestem pewien, czy to właśnie miałeś na myśli mówiąc o duplikacie, ale:

return new HomeController().Index();

Edytować

Inną opcją może być utworzenie własnej fabryki ControllerFactory, w ten sposób można określić, który kontroler ma zostać utworzony.

JoshBerke
źródło
to może być podejście, ale wydaje się, że nie ma odpowiedniego kontekstu - nawet jeśli powiem hc.ControllerContext = this.ControllerContext. Dodatkowo szuka widoku w ~ / Views / Gateway / 5.aspx i nie znajduje go.
Simon_Weaver
Dodatkowo tracisz wszystkie filtry akcji. Prawdopodobnie chcesz spróbować użyć metody Execute w interfejsie IController, który muszą zaimplementować kontrolery. Na przykład: ((IController) new HomeController ()). Execute (...). W ten sposób nadal uczestniczysz w potoku Action Invoker. Musiałbyś jednak dokładnie wymyślić, co przekazać do wykonania ... Reflector może w tym pomóc :)
Andrew Stanton-Nurse
Tak, nie podoba mi się pomysł unowocześniania kontrolera, myślę, że lepiej będzie zdefiniować własną fabrykę kontrolerów, która wydaje się być odpowiednim punktem rozszerzenia do tego. Ale ledwo zarysowałem powierzchnię tego szkieletu, więc mogę być daleko.
JoshBerke
1

Czy routing nie zajmuje się tylko tym scenariuszem? tj. dla scenariusza opisanego powyżej, możesz po prostu utworzyć procedurę obsługi trasy, która zaimplementowała tę logikę.

Richard
źródło
opiera się na warunkach programowych. tj. kampania 100 może przejść do widoku 7, a kampania 200 może przejść do widoku 8 itd., itd. zbyt skomplikowane, aby przekierować
Simon_Weaver
4
Dlaczego jest to zbyt skomplikowane do routingu? Co jest nie tak z niestandardowymi ograniczeniami trasy? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer
1

Dla każdego, kto używa routingu opartego na wyrażeniach i używa tylko powyższej klasy TransferResult, oto metoda rozszerzenia kontrolera, która załatwia sprawę i zachowuje TempData. Nie ma potrzeby 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
źródło
Ostrzeżenie: wydaje się, że powoduje to błąd „Klasa SessionStateTempDataProvider wymaga włączenia stanu sesji”, chociaż w rzeczywistości nadal działa. Widzę ten błąd tylko w moich dziennikach. Używam ELMAH do rejestrowania błędów i otrzymuję ten błąd dla InProc i AppFabric
Simon_Weaver
1

Server.TransferRequestjest całkowicie niepotrzebne w MVC . Jest to przestarzała funkcja, która była konieczna tylko w ASP.NET, ponieważ żądanie przyszło bezpośrednio do strony i musiał istnieć sposób na przesłanie żądania na inną stronę. Nowoczesne wersje ASP.NET (w tym MVC) mają infrastrukturę routingu, którą można dostosować w celu kierowania bezpośrednio do żądanego zasobu. Nie ma sensu pozwalać, aby żądanie dotarło do kontrolera tylko po to, aby przesłać je do innego kontrolera, gdy możesz po prostu skierować żądanie bezpośrednio do kontrolera i żądanej akcji.

Co więcej, ponieważ odpowiadasz na pierwotne żądanie, nie ma potrzeby wkładania czegokolwiek do TempDatalub innego magazynu tylko po to, aby skierować żądanie we właściwe miejsce. Zamiast tego dochodzisz do akcji kontrolera z nienaruszonym pierwotnym żądaniem. Możesz również mieć pewność, że Google zaakceptuje to podejście, ponieważ dzieje się to całkowicie po stronie serwera.

Chociaż możesz zrobić sporo z obu IRouteConstrainti IRouteHandler, najpotężniejszym punktem rozszerzenia dla routingu jest RouteBasepodklasa. Tę klasę można rozszerzyć, aby zapewniała zarówno trasy przychodzące, jak i generowanie wychodzących adresów URL, co sprawia, że ​​jest to punkt kompleksowej obsługi wszystkiego, co ma związek z adresem URL i akcją wykonywaną przez URL.

Tak więc, aby podążać za drugim przykładem, aby dostać się z /do /home/7, potrzebujesz po prostu trasy, która dodaje odpowiednie wartości trasy.

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

Ale wracając do pierwotnego przykładu, w którym masz losową stronę, jest to bardziej złożone, ponieważ parametry trasy nie mogą się zmieniać w czasie wykonywania. Więc można to zrobić z RouteBasepodklasą w następujący sposób.

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

Które można zarejestrować w routingu, na przykład:

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

Zauważ, że w powyższym przykładzie sensowne może być również przechowywanie pliku cookie rejestrującego wersję strony głównej, na którą wszedł użytkownik, aby po powrocie otrzymali tę samą wersję strony głównej.

Należy również zauważyć, że przy użyciu tego podejścia można dostosować routing, aby uwzględnić parametry ciągu zapytania (domyślnie całkowicie je ignoruje) i odpowiednio skierować do odpowiedniej akcji kontrolera.

Dodatkowe przykłady

NightOwl888
źródło
A co, jeśli nie chcę przenosić się natychmiast po wejściu w akcję, ale raczej pozwolić tej akcji wykonać jakąś pracę, a następnie warunkowo przenieść się do innej akcji. Zmiana mojego routingu na bezpośredni transfer do celu transferu nie zadziała, więc wygląda na to, że Server.TransferRequestnie jest w końcu „całkowicie niepotrzebna w MVC”.
ProfK
0

Nie jest to odpowiedź sama w sobie, ale oczywiście wymaganie polegałoby nie tylko na tym, aby rzeczywista nawigacja „wykonywała” równoważną funkcjonalność Webforms Server.Transfer (), ale także aby to wszystko było w pełni obsługiwane w ramach testów jednostkowych.

Dlatego ServerTransferResult powinien "wyglądać" jak RedirectToRouteResult i być możliwie jak najbardziej podobny pod względem hierarchii klas.

Myślę o zrobieniu tego, patrząc na Reflector i robiąc wszystko, co robią klasa RedirectToRouteResult, a także różne metody klasy bazowej kontrolera, a następnie „dodając” to drugie do kontrolera za pomocą metod rozszerzających. Może mogłyby to być metody statyczne w tej samej klasie, dla ułatwienia / lenistwa w pobieraniu?

Jeśli się do tego zabiorę, opublikuję to, w przeciwnym razie może ktoś inny mnie pobije!

William
źródło
0

Osiągnąłem to, wykorzystując Html.RenderActionpomocnika w widoku:

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

A w moim kontrolerze:

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