Як імітувати Server.Transfer в ASP.NET MVC?


124

У ASP.NET MVC ви можете повернути перенаправлення ActionResult досить легко:

 return RedirectToAction("Index");

 or

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

Це фактично дасть переспрямування HTTP, що зазвичай нормально. Однак при використанні аналітики Google це спричиняє великі проблеми, оскільки початковий референт втрачається, тому Google не знає, звідки ви прийшли. Це втрачає корисну інформацію, таку як будь-які пошукові терміни.

Як зауваження, цей метод має перевагу в тому, щоб видалити будь-які параметри, які можуть бути отримані з кампаній, але все ж дозволяє мені захоплювати їх стороною сервера. Залишаючи їх у рядку запиту, люди призводять до закладки чи щебетування або блогу посилання, яке їм не слід. Я бачив це кілька разів, коли люди перекручували посилання на наш сайт, що містять ідентифікатори кампанії.

У будь-якому разі я пишу контролер «шлюзу» для всіх вхідних відвідувань сайту, який я можу переадресувати в різні місця або альтернативні версії.

На даний момент я більше дбаю про Google (ніж випадкові закладки), і я хочу, щоб я могла надсилати того, хто відвідує /сторінку, яку вони отримають, якби перейти /home/7, а це версія 7 домашньої сторінки.

Як я вже говорив раніше Якщо я роблю це, я втрачаю здатність google аналізувати реферера:

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

Те, що я дійсно хочу, це

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

що дасть мені такий погляд без перенаправлення на сторону клієнта. Я не думаю, що таке існує.

В даний час найкраще, що я можу придумати, - це дублювати всю логіку контролера HomeController.Index(..)в моїй GatewayController.Indexдії. Це означає , що я повинен був рухатися 'Views/Home'в 'Shared'так що це було доступно. Має бути кращий спосіб ?? ..


Що саме таке, ServerTransferActionщо ви намагалися повторити? Це реальна річ? (не вдалося знайти жодної інформації про нього ... дякую за запитання, btw, відповідь нижче чудова)
jleach

Знайдіть сервер.передача (...). Це в основному зробити "переадресацію" на стороні сервера, де клієнт отримує перенаправлену сторінку без перенаправлення на сторону клієнта. Як правило, це не рекомендується при сучасній маршрутизації.
Simon_Weaver

1
"Передача" - це застаріла функція ASP.NET, яка більше не потрібна в MVC через можливість безпосередньо переходити до правильних дій контролера за допомогою маршрутизації. Детальніше див. У цій відповіді .
NightOwl888

@ NightOwl888 так безумовно - але також іноді через ділову логіку це потрібно / простіше. Я озирнувся назад, щоб побачити, де я в кінцевому підсумку скористався цим - (на щастя, це було лише в одному місці) - де у мене є домашня сторінка, яка хотіла бути динамічною для певних складних умов, і тому за лаштунками вона показує інший маршрут. Безумовно, хочеться максимально уникати цього на користь маршрутів або умов маршруту, але іноді просте ifтвердження є занадто спокусливим рішенням.
Simon_Weaver

@Simon_Weaver - А що не так з підкласифікацією, RouteBaseщоб ви могли поставити ifтам свою заяву, а не згинати все назад, щоб перейти з одного контролера на інший?
NightOwl888

Відповіді:


130

Як щодо класу TransferResult? (на основі відповіді Станса )

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

Оновлено: Зараз працює з MVC3 (використовуючи код з поста Саймона ). Він повинен був (не в змозі перевірити його) також працювати в MVC2, дивлячись на те, працює він чи ні в рамках інтегрованого конвеєра IIS7 +.

Для повної прозорості; У нашому виробничому середовищі ми ніколи не використовуємо TransferResult безпосередньо. Ми використовуємо TransferToRouteResult, який, у свою чергу, виконує TransferResult. Ось що насправді працює на моїх виробничих серверах.

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

І якщо ви використовуєте T4MVC (якщо ні ... робіть!), Це розширення може стати в нагоді.

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

Використовуючи цей маленький самоцвіт, який ви можете зробити

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

1
це чудово працює. будьте обережні, щоб не закінчитись нескінченним циклом - як я це зробив у першій моїй спробі, передавши неправильну URL-адресу. Я зробив невелику модифікацію, щоб дозволити передачу значень маршруту, яка може бути корисною для інших. розміщено вище чи нижче ...
Simon_Weaver

оновлення: це рішення, здається, працює добре, і хоча я використовую його лише в дуже обмеженій ємності, ще не знайшло жодних проблем
Simon_Weaver

одне питання: не можна перенаправляти з POST на GET запит - але це не обов'язково погано. з чим бути обережним
Simon_Weaver

2
@BradLaney: Ви можете просто видалити рядки 'var urlHelper ...' та 'var url ...' та замінити 'url' на 'this.Url' на решту, і це працює. :)
Майкл Ульман

1
1: з'єднання / тестування блоку / сумісність у майбутньому. 2: зразки mvc core / mvc ніколи не використовують цей синглтон. 3: цей сингл недоступний у потоці (null), ні пулі пулу, ні делегата асинхронизації, викликаному в контексті, відмінному від за замовчуванням, як, наприклад, при використанні методів дії async. 4: лише для цілей сумісності, mvc встановлює це значення однотонного контексту.HttpContext перед введенням коду користувача.
Softlion

47

Редагувати: оновлено, щоб бути сумісним з ASP.NET MVC 3

За умови використання IIS7 наступна модифікація, здається, працює для ASP.NET MVC 3. Завдяки @nitin та @andy за вказівку оригінального коду не працювало.

Редагувати 4.11.2011: TempData розривається з сервером.TransferRequest станом на MVC 3 RTM

Змінено код нижче, щоб кинути виняток, але наразі немає іншого рішення.


Ось моя модифікація, заснована на модифікованій Маркусом версії оригінальної публікації Стана. Я додав додатковий конструктор, щоб взяти словник Route Value - і перейменував його на MVCTransferResult, щоб уникнути плутанини, що це може бути просто переадресація.

Тепер я можу зробити наступне для переадресації:

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

Мій модифікований клас:

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
Схоже, це не працює в MVC 3 RC. Помилка HttpHandler.ProcessRequest (), говорить: "HttpContext.SetSessionStateBehavior" можна викликати лише до того, як подія "HttpApplication.AcquireRequestState" буде піднята.
Енді

у мене ще не було змін, щоб подивитися на MVC3. повідомте мені, якщо ви знайдете рішення
Simon_Weaver

Чи Server.TransferRquest, як пропонує Нітін, робить те, що намагається зробити вище?
Старий Гейзер

Чому нам потрібно перевірити TempData на null і підрахувати> 0?
Юрарт

Ви цього не зробите, але це лише функція безпеки, тому якщо ви вже користуєтесь нею і покладаєтесь на неї, то вам не залишиться чесати голову, якщо вона зникне
Simon_Weaver


12

Нещодавно я дізнався, що ASP.NET MVC не підтримує Server.Transfer (), тому я створив метод заглушки (надихнувши 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

Не могли ви просто створити екземпляр контролера, на який ви хочете переспрямувати, викликати потрібний метод дії, а потім повернути результат цього? Щось на зразок:

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

4
Ні, у створеному вами контролері не буде правильно встановити запити та відповіді. Це може призвести до проблем.
Джефф Уокер Код Рейнджер

Я згоден з @JeffWalkerCodeRanger: те саме, що і після встановлення власностіotherController.ControllerContext = this.ControllerContext;
T-moty

7

Я хотів перенаправити поточний запит до іншого контролера / дії, зберігаючи шлях виконання точно таким же, як якщо б був запрошений цей другий контролер / дія. У моєму випадку Server.Request не працюватиме, оскільки я хотів додати більше даних. Це фактично еквівалент поточному обробнику, який виконує інший HTTP GET / POST, а потім передає результати клієнту. Я впевнений, що будуть кращі способи досягти цього, але ось що для мене працює:

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

Ваша здогадка правильна: я поклав цей код

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

і я використовую його для відображення помилок розробникам, в той час як він буде використовувати звичайне переспрямування у виробництві. Зауважте, що я не хотів використовувати сеанс ASP.NET, базу даних чи інші способи передачі даних про виключення між запитами.


7

Замість того, щоб імітувати передачу сервера, MVC все ще здатний реально робити Server.TransferRequest :

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

Не соромтесь додати текст у свою відповідь, щоб пояснити його далі.
Володимир Палант

Зверніть увагу, для цього потрібен MVCv3 і вище.
Seph

5

Просто встановіть інший контролер і виконайте його метод дії.


Це не відобразить потрібну URL-адресу в адресному рядку
arserbin3

@ arserbin3 - Сервер не передаватиметься. Ця вимога, мабуть, є тим, чому оригінальне запитання навіть було розміщено.
Річард Шалай

2

Ви можете створити інший контролер і викликати метод дії, що повертає результат. Це вимагає, щоб ви розмістили свій погляд у загальній папці.

Я не впевнений, чи це ви мали на увазі дублікат, але:

return new HomeController().Index();

Редагувати

Іншим варіантом може бути створення власного ControllerFactory, таким чином ви можете визначити, який контролер створити.


це може бути підхід, але, здається, цілком не має контексту, навіть якщо я скажу hc.ControllerContext = this.ControllerContext. Крім того, він шукає подання під ~ / Views / Gateway / 5.aspx і не знаходить його.
Simon_Weaver

Плюс ви втрачаєте всі фільтри дій. Можливо, ви хочете спробувати скористатися методом Execute в інтерфейсі IController, який повинні реалізувати ваші контролери. Наприклад: ((IController) новий HomeController ()). Виконати (...). Таким чином, ти все ще береш участь у трубопроводі Action Invoker. Вам би довелося зрозуміти, що саме потрібно передати на Execute, хоча ... Reflector може допомогти там :)
Ендрю Стентон-Медсестра

Так, мені не подобається ідея нововведення контролера, я думаю, що вам краще визначити власну фабрику контролерів, яка, здається, є правильною точкою розширення для цього. Але я ледве не подряпав поверхню цієї рамки, щоб я міг бути далеко.
JoshBerke

1

Чи не маршрутизація просто не піклується про цей сценарій для вас? тобто для описаного вище сценарію ви можете просто створити обробник маршруту, який реалізував цю логіку.


її засновані на програмних умовах. тобто кампанія 100 може перейти до перегляду 7, а кампанія 200 може перейти до перегляду 8 і т.д. тощо. занадто складна для маршрутизації
Simon_Weaver

4
Чому це занадто складно для маршрутизації? Що не так із спеціальними обмеженнями маршруту? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer

1

Для тих, хто використовує маршрутизацію на основі виразів, використовуючи лише клас TransferResult, наведений вище, ось метод розширення контролера, який виконує трюк та зберігає TempData. Не потрібно 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);
}

Попередження: це, мабуть, спричиняє помилку "Клас SessionStateTempDataProvider вимагає ввімкнути стан сеансу", хоча він фактично все ще працює. Я бачу цю помилку лише в своїх журналах. Я використовую ELMAH для реєстрації помилок і отримую цю помилку для InProc та AppFabric
Simon_Weaver

1

Server.TransferRequestє абсолютно непотрібним в MVC . Це застаріла функція, яка була необхідна лише в ASP.NET, оскільки запит надходив безпосередньо на сторінку, і там був потрібний спосіб перенесення запиту на іншу сторінку. Сучасні версії ASP.NET (включаючи MVC) мають інфраструктуру маршрутизації, яку можна налаштувати для маршруту безпосередньо до потрібного ресурсу. Немає сенсу дозволяти запиту звертатися до контролера лише для того, щоб передати його іншому контролеру, коли ви можете просто зробити так, щоб запит перейшов безпосередньо до контролера та дій, які ви хочете.

Більше того, оскільки ви відповідаєте на оригінальний запит, немає необхідності вкладати щось у TempDataінший сховище лише задля того, щоб направити запит у потрібне місце. Натомість ви доходите до дії контролера з оригінальним запитом неушкодженим. Ви також можете бути впевнені, що Google схвалить цей підхід, оскільки це відбувається повністю на стороні сервера.

Хоча ви можете зробити трохи з обох, IRouteConstraintі IRouteHandler, найпотужнішою точкою розширення для маршрутизації є RouteBaseпідклас. Цей клас може бути розширений, щоб забезпечити як вхідні маршрути, так і вихідні URL-адреси, що робить це єдиним зупинком для всього, що стосується URL-адреси та дій, які виконує URL.

Отже, щоб наслідувати ваш другий приклад, щоб дістатися /до /home/7, вам просто потрібен маршрут, який додає відповідні значення маршруту.

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

Але повертаючись до оригінального прикладу, де у вас є випадкова сторінка, вона є більш складною, оскільки параметри маршруту не можуть змінюватися під час виконання. Отже, це можна зробити з RouteBaseпідкласом наступним чином.

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

Які можуть бути зареєстровані в маршрутизації, як:

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

Зауважте у наведеному вище прикладі, може бути доцільним також зберігати файл cookie, який записує версію домашньої сторінки, на яку увійшов користувач, тому після повернення вони отримують ту саму версію домашньої сторінки.

Зауважте також, що, використовуючи цей підхід, ви можете налаштувати маршрутизацію, щоб врахувати параметри рядка запиту (він повністю ігнорує їх за замовчуванням) та відповідно прокладати відповідну дію контролера.

Додаткові приклади


Що робити, якщо я не хочу перенести відразу після вступу в дію, а краще дозволити цій дії виконати певну роботу, а потім умовно перейти до іншої дії. Зміна моєї маршрутизації, щоб перейти безпосередньо до цілі передачі, не спрацює, тому, схоже, Server.TransferRequestце не є "зовсім непотрібним у MVC".
ПрофК

0

Це не відповідь сама по собі, але очевидно, що вимога полягатиме не лише в тому, щоб фактична навігація "виконала" еквівалентну функціональність Webforms Server.Transfer (), але і щоб все це було повністю підтримане в рамках тестування одиниць.

Тому ServerTransferResult повинен "виглядати" як RedirectToRouteResult і бути максимально схожим з точки зору ієрархії класів.

Я думаю зробити це, дивлячись на Reflector і виконуючи будь-який клас RedirectToRouteResult, а також різні методи базового класу Controller, і потім "додавати" останнього до Controller методами розширення. Можливо, це можуть бути статичні методи в межах одного класу для зручності / ліні завантаження?

Якщо я обійдуся цим, я опублікую його, інакше, можливо, хтось інший може перемогти мене до цього!


0

Я досяг цього, використовуючи Html.RenderActionпомічника в Погляді:

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

І в моєму контролері:

public ActionResult MyAction(....)
{
    var routeValues = HttpContext.Request.RequestContext.RouteData.Values;    
    ViewBag.ActionName = "myaction";
    ViewBag.ControllerName = "mycontroller";
    ViewBag.RouteValues = routeValues;    
    return PartialView("_AjaxRedirect");
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.