Как имитировать 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', чтобы он был доступен. Должен быть лучший способ?..

Ответ 1

Как насчет класса TransferResult? (на основе Отвечает 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);
        }
    }
}

Обновлено: Теперь работает с 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());

Ответ 2

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

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

Редактировать 4/11/2011: TempData прерывается с Server.TransferRequest от RTM MVC 3

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


Здесь моя модификация основана на модифицированной версией Stan Stan Post Post. Я добавил дополнительный конструктор, чтобы взять словарь значений маршрута - и переименовал его в 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);
    }
}

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

Просто введите другой контроллер и выполните его действие.

Ответ 8

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

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

Ответ 9

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

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

return new HomeController().Index();

Edit

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

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

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

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