如何在ASP.NET MVC中模拟Server.Transfer?


124

在ASP.NET MVC中,您可以很容易地返回重定向ActionResult:

 return RedirectToAction("Index");

 or

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

实际上,这将提供HTTP重定向,通常很好。但是,在使用Google Analytics(分析)时,这会引起很大的问题,因为原始引荐来源网址丢失了,因此Google不知道您来自何处。这会丢失有用的信息,例如任何搜索引擎术语。

附带说明一下,此方法的优点是删除了广告系列可能带来的所有参数,但仍然允许我在服务器端捕获它们。将其留在查询字符串中会导致人们为他们添加书签或在Twitter或Blog中添加他们不应该使用的链接。我已经看过几次了,人们在Twitter上发布了指向包含广告系列ID的网站的链接。

无论如何,我正在为该站点的所有传入访问编写一个“网关”控制器,我可能会将其重定向到其他地方或其他版本。

就目前而言,我现在更关心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'以便可以访问。肯定有更好的办法??..


ServerTransferAction您要复制的确切是什么?那是真的吗?(找不到任何信息...感谢您的问题,顺便说一句,以下答案非常棒)
jleach

查找Server.Transfer(...)。这是一种基本在服务器端执行“重定向”的方法,客户端无需客户端重定向即可接收重定向页面。通常,现代路由不建议使用。
Simon_Weaver

1
“传输”是一种过时的ASP.NET功能,由于能够使用路由直接进入正确的控制器操作,因此在MVC中不再需要此功能。有关详细信息,请参见此答案
NightOwl888 '18

@ NightOwl888是肯定的-但有时由于业务逻辑的原因,这是必要/容易的。我回头看了看,最终在哪里使用了它-(幸运的是,它只在一个地方)-在那儿我有一个主页,希望在某些复杂条件下保持动态,因此在后台它显示了一条不同的路线。绝对希望尽可能地避免使用路由或路由条件,但有时使用简单的if语句就很容易解决。
Simon_Weaver

@Simon_Weaver-子类化又有什么问题,RouteBase因此您可以将if语句放在此处,而不必将所有内容都向后弯曲以从一个控制器跳转到另一个控制器?
NightOwl888 '18

Answers:


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(使用Simon的文章中的代码)。通过查看它是否正在IIS7 +的集成管道中运行,它应该(无法对其进行测试)也可以在MVC2中工作。

完全透明;在我们的生产环境中,我们从未直接使用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核心/ mvc样本永远不要使用此单例。3:此单例在线程(空),池线程或在默认值以外的上下文中调用的异步委托中均不可用,例如在使用异步操作方法时。4:仅出于兼容性目的,mvc在输入用户代码之前将此单例值设置为context.HttpContext。
Softlion

47

编辑:更新为与ASP.NET MVC 3兼容

如果您使用的是IIS7,则以下修改似乎适用于ASP.NET MVC3。感谢@nitin和@andy指出原始代码无效。

编辑4/11/2011:从MVC 3 RTM开始,TempData与Server.TransferRequest断开

修改了下面的代码以引发异常-但目前没有其他解决方案。


这是我根据Markus对Stan原始帖子的修改版本所做的修改。我添加了一个附加的构造函数以使用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 2010年

由Nitin建议的Server.TransferRquest是否可以执行以上操作?
老盖泽

为什么我们需要检查TempData的null和count> 0?
yurart '17

您没有,但它只是安全功能,因此,如果您已经在使用它并依靠它,那么它消失了,您也不会挠头
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 2015年

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
}

随时在您的答案中添加一些文字以进一步解释。
Wladimir Palant 2014年

请注意,这需要MVCv3及更高版本。
Seph 2014年


2

您可以重新安装另一个控制器,然后调用操作方法以返回结果。但是,这将需要您将视图放入共享文件夹中。

我不确定这是否是重复的意思,但是:

return new HomeController().Index();

编辑

另一个选择是创建自己的ControllerFactory,这样您就可以确定要创建哪个控制器。


这可能是一种方法,但是它似乎没有足够的上下文权限-即使我说hc.ControllerContext = this.ControllerContext。另外,它还会在〜/ Views / Gateway / 5.aspx下查找视图,但找不到它。
Simon_Weaver

另外,您将丢失所有的动作筛选器。您可能想尝试在控制器必须实现的IController接口上使用Execute方法。例如:((IController)new HomeController())。Execute(...)这样,您仍然可以参与Action Invoker管道。您必须确切地知道要传递给Execute的内容... Reflector可能会帮助您:)
Andrew Stanton-Nurse 2009年

是的,我不喜欢更新控制器的想法,我认为最好定义自己的控制器工厂,这似乎是正确的扩展点。但是我几乎没有刮过这个框架的表面,所以我可能会遥遥无期。
09年


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中完全没有必要”。
ProfK

0

本身不是一个答案,但是很明显,要求不仅是实际导航“执行” Webforms Server.Transfer()的等效功能,而且所有这些都必须在单元测试中得到完全支持。

因此,ServerTransferResult应该“看起来”像RedirectToRouteResult,并且在类层次结构上尽可能相似。

我正在考虑通过查看Reflector,执行RedirectToRouteResult类以及各种Controller基类方法来执行此操作,然后通过扩展方法将后者“添加”到Controller中。为了轻松/轻松地下载,这些可能是同一类中的静态方法吗?

如果我愿意这样做,我会把它发布出来,否则也许有人会击败我!


0

我通过Html.RenderAction在View中利用助手来实现此目的:

@{
    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");
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.