Как смоделировать 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.IndexAction. Это означает , что я должен был двигаться 'Views/Home'в 'Shared'так что это было доступно. Должен быть способ получше ?? ..

Simon_Weaver
источник
Что именно ServerTransferActionвы пытались воспроизвести? Это актуально? (не смог найти никакой информации об этом ... спасибо за вопрос, кстати, ответ ниже превосходен)
jleach
Найдите Server.Transfer (...). Это способ выполнить «перенаправление» на стороне сервера, когда клиент получает перенаправленную страницу без перенаправления на стороне клиента. Обычно это не рекомендуется при современной маршрутизации.
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 07
обновление: похоже, это решение работает хорошо, и хотя я использую его только в очень ограниченном
объеме,
одна проблема: невозможно перенаправить с запроса POST на запрос GET - но это не обязательно плохо. кое-что, с чем следует быть осторожным
Simon_Weaver
2
@BradLaney: Вы можете просто удалить строки 'var urlHelper ...' и 'var url ...' и заменить 'url' на 'this.Url' для остальных, и это сработает. :)
Майкл Ульманн
1
1: соединение / модульное тестирование / будущая совместимость. 2: образцы mvc core / mvc никогда не используют этот синглтон. 3: этот синглтон недоступен в потоке (null), ни в потоке пула, ни в асинхронном делегате, вызываемом в контексте, отличном от значения по умолчанию, например, при использовании методов асинхронного действия. 4: только для целей совместимости mvc устанавливает это одноэлементное значение в context.HttpContext перед вводом кода пользователя.
Softlion 03
47

Изменить: обновлено для совместимости с ASP.NET MVC 3

При условии, что вы используете IIS7, следующая модификация, похоже, работает для ASP.NET MVC 3. Спасибо @nitin и @andy за указание на то, что исходный код не работал.

Изменить 4/11/2011: TempData ломается с Server.TransferRequest с MVC 3 RTM

Изменил приведенный ниже код, чтобы создать исключение, но на данный момент другого решения нет.


Вот моя модификация, основанная на модифицированной версии Маркуса оригинального сообщения Стэна. Я добавил дополнительный конструктор, чтобы взять словарь значений маршрута - и переименовал его в 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);
    }
}
Simon_Weaver
источник
1
Кажется, это не работает в MVC 3 RC. Сбой в HttpHandler.ProcessRequest (), говорит: «HttpContext.SetSessionStateBehavior» может быть вызван только до возникновения события «HttpApplication.AcquireRequestState».
Энди
у меня еще не было изменений, чтобы посмотреть на MVC3. дайте мне знать, если найдете решение
Simon_Weaver
Выполняет ли Server.TransferRquest, предложенный Nitin, то, что пытается сделать вышеприведенное?
Old Geezer
Почему нам нужно проверять TempData на null и count> 0?
yurart
Вы этого не делаете, но это просто функция безопасности, поэтому, если вы уже используете ее и полагаетесь на нее, вам не придется чесать голову, если она исчезнет
Simon_Weaver 01
14

Вместо этого вы можете использовать Server.TransferRequest в IIS7 +.

Нитин Агарвал
источник
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
Нет, на создаваемом вами контроллере не будет правильной настройки запросов и ответов. Это может привести к проблемам.
Джефф Уокер Code Ranger
Я согласен с @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
}
AaronLS
источник
Не стесняйтесь добавлять текст к своему ответу, чтобы объяснить его дальше.
Владимир Палант
Обратите внимание, для этого требуется MVCv3 и выше.
Seph
5

Просто создайте экземпляр другого контроллера и выполните его метод действия.

Ричард Салай
источник
Это не приведет к отображению желаемого URL в адресной строке
arserbin3
@ arserbin3 - Server.Transfer тоже не будет. Это требование, по-видимому, является причиной того, что исходный вопрос был опубликован.
Ричард Салай
2

Вы можете обновить другой контроллер и вызвать метод действия, возвращающий результат. Однако это потребует от вас поместить свое представление в общую папку.

Я не уверен, что вы имели в виду дубликат, но:

return new HomeController().Index();

редактировать

Другой вариант - создать собственный ControllerFactory, таким образом вы сможете определить, какой контроллер создать.

JoshBerke
источник
это может быть подход, но он, похоже, не совсем подходит для контекста - даже если я скажу 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/…
Ян Мерсер,
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, записывающий версию домашней страницы, на которой зашел пользователь, чтобы при возвращении они получали ту же версию домашней страницы.

Также обратите внимание, что, используя этот подход, вы можете настроить маршрутизацию, чтобы учитывать параметры строки запроса (он полностью игнорирует их по умолчанию) и соответствующим образом направлять к соответствующему действию контроллера.

Дополнительные примеры

NightOwl888
источник
Что делать, если я не хочу сразу переходить при вводе действия, а просто позволяю этому действию выполнять некоторую работу, а затем условно переходить к другому действию. Изменение моей маршрутизации для прямого перехода к цели передачи не сработает, поэтому похоже, что 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");
}
Colin
источник