Как смоделировать Server.Transfer в ASP.NET MVC?

В ASP.NET MVC вы можете довольно легко вернуть перенаправленный ActionResult:

 return RedirectToAction("Index");

 or

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

Это фактически даст перенаправление HTTP, что обычно нормально. Однако при использовании Google Analytics это вызывает большие проблемы, потому что первоначальный реферер потерян, поэтому 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' так что было доступно. Должен быть лучший способ??..

14 ответов

Решение

Как насчет класса 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());

Изменить: Обновлен для совместимости с 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);
    }
}

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

Недавно я обнаружил, что 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);
    }

Вместо того, чтобы имитировать передачу с сервера, MVC по-прежнему способен фактически выполнять Server.TransferRequest:

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

Не могли бы вы просто создать экземпляр контроллера, на который вы хотите перенаправить, вызвать метод действия, который вы хотите, а затем вернуть результат этого? Что-то вроде:

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

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

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

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

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

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

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

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

return new HomeController().Index();

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

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

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

Для тех, кто использует маршрутизацию на основе выражений и использует только вышеприведенный класс 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);
}

Я достиг этого, используя 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");
}

По сути, это не ответ, но очевидно, что требовалось бы, чтобы фактическая навигация "не делала" эквивалентную функциональность Webforms Server.Transfer(), но также чтобы все это полностью поддерживалось в модульном тестировании.

Поэтому ServerTransferResult должен "выглядеть" как RedirectToRouteResult и быть максимально похожим с точки зрения иерархии классов.

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

Если мне удастся сделать это, я опубликую это, иначе, может быть, кто-то другой может побить меня этим!

Другие вопросы по тегам