Как смоделировать Server.Transfer в ASP.NET MVC? - PullRequest
121 голосов
/ 28 апреля 2009

В 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 ]

129 голосов
/ 25 июля 2009

Как насчет класса 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());
47 голосов
/ 07 августа 2009

Редактировать: Обновлено для совместимости с 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);
    }
}
13 голосов
/ 18 сентября 2010

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

12 голосов
/ 21 мая 2009

Недавно я обнаружил, что 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 голосов
/ 28 апреля 2009

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

 HomeController controller = new HomeController();
 return controller.Index();
7 голосов
/ 18 октября 2009

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

6 голосов
/ 04 января 2014

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

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
5 голосов
/ 28 апреля 2009

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

2 голосов
/ 28 апреля 2009

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

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

return new HomeController().Index();

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

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

1 голос
/ 20 января 2018

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

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

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

...