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

127
在ASP.NET MVC中,您可以很容易地返回重定向ActionResult:
 return RedirectToAction("Index");

 or

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

这实际上会产生一个HTTP重定向,通常情况下没有问题。但是,当使用Google Analytics时,这会导致大问题,因为原始引用丢失了,所以谷歌不知道你来自哪里。这会丢失有用的信息,例如任何搜索引擎词条。

顺便说一句,这种方法的优点是可以删除可能来自广告系列的任何参数,但仍允许我在服务器端捕获它们。将它们留在查询字符串中会导致人们收藏或在Twitter或博客上发布不应该的链接。我已经看到过几次这样的情况,人们在Twitter上发布包含广告ID的我们网站链接。

无论如何,我正在为所有进入网站的访问编写“网关”控制器,我可以将其重定向到不同的位置或替代版本。

现在,我更关心谷歌(而非意外收藏夹),我希望能够将访问“ /”网址的人发送到他们如果去到“ /home/7”将得到的页面,这是主页的第7个版本。

就像我之前说的那样,如果我这样做,我将失去让Google分析引荐来源的能力:

 return RedirectToAction(new { controller = "home", version = 7 });
我真正想要的是一个。
 return ServerTransferAction(new { controller = "home", version = 7 });

我认为没有一种方法能够在无需客户端重定向的情况下得到那个视图。

目前我能想到的最好的办法是在我的GatewayController.Index动作中复制整个HomeController.Index(..)的控制器逻辑。 这意味着我必须将'Views/Home'移动到'Shared'中以使其可访问。 一定有更好的方法。


你试图复制的 ServerTransferAction 到底是什么?那是一个真实存在的东西吗?(我找不到任何相关信息...顺便感谢你的问题,下面的答案非常好) - jleach
1
查找 Server.Transfer(...)。这是一种在服务器端基本上执行“重定向”的方式,客户端接收重定向页面而无需进行客户端重定向。通常情况下,现代路由不建议使用它。 - Simon_Weaver
1
“Transferring”是过时的ASP.NET功能,在MVC中不再需要,因为可以使用路由直接访问正确的控制器操作。有关详细信息,请参见此答案 - NightOwl888
@NightOwl888 是的,绝对没错 - 但有时基于业务逻辑考虑会更加必要/容易。我回头看了一下自己在哪里使用过这个方法 - (幸运的是只有一个地方)- 在那里我有一个首页,希望根据某些复杂条件进行动态处理,因此在后台显示不同的路径。尽可能避免使用它,而选择路由或路由条件 - 但有时候简单的 if 语句确实是一个太诱人的解决方案。 - Simon_Weaver
@Simon_Weaver - 如果你可以通过子类化RouteBase来放置你的if语句,而不是为了从一个控制器跳到另一个控制器而弯曲一切,那么这有什么问题吗? - NightOwl888
14个回答

132

那么一个TransferResult类怎么样?(基于Stans answer

/// <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
1
警告:Server.TransferRequest与TempData不兼容。虽然在运行时您看不到任何问题(除了TempData为空),但Application_Error会被触发。有关详细信息,请参见我的答案。抱歉,这些答案列表变得非常复杂! - Simon_Weaver
2
@BradLaney:你可以删除'var urlHelper...'和'var url...'这两行,然后将'replace 'url' with 'this.Url''应用于其余部分,就可以正常工作了。 :) - Michael Ulmann
var httpContext = HttpContext.Current; 是一个恶梦!!!在 asp.net mvc 中使用 context.HttpContext,永远不要使用 HttpContext.Current!!! - Softlion
1
1: 耦合/单元测试/未来兼容性。2: MVC核心/MVC示例从不使用此单例模式。3: 此单例模式在线程中不可用(为null),无论是池线程还是在使用异步操作方法时调用的非默认上下文的异步委托。4: 仅为了兼容性,MVC在进入用户代码之前将此单例模式值设置为context.HttpContext。 - Softlion
显示剩余8条评论

48

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

如果您正在使用IIS7,则以下修改似乎适用于ASP.NET MVC 3。感谢@nitin和@andy指出原始代码无法工作。

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

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


这是我基于Markus对Stan原始帖子的修改所做的修改。我添加了一个额外的构造函数来采用路由值字典-并将其重命名为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" 事件之前调用。 - Andy
我还没有机会看MVC3。如果你找到了解决方案,请告诉我。 - Simon_Weaver
Nitin所建议的Server.TransferRequest是否能够实现上述操作? - Old Geezer
为什么我们需要检查TempData是否为空并且计数> 0? - yurart
你不需要,但这只是一个安全功能,如果你已经在使用它并依赖它,那么如果它消失了,你就不会感到困惑。 - Simon_Weaver
看起来 .Count 是一个属性而不是方法,所以不需要加 ()。 - Dronz

15
你可以在 IIS7+ 上使用 Server.TransferRequest

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();

6
不,您创建的控制器将无法正确设置Request和Response等内容。这可能会导致问题。 - Jeff Walker Code Ranger
1
我同意@JeffWalkerCodeRanger的观点:在设置属性otherController.ControllerContext = this.ControllerContext;之后也是一样的。 - T-moty

9
与模拟服务器传输不同,MVC仍然能够实际执行Server.TransferRequest
public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}

请随意在您的答案中添加一些文本以进一步解释它。 - Wladimir Palant
请注意,此功能需要MVCv3版本及以上。 - Seph

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会话、数据库或其他方法在请求之间传递异常数据。


5

只需实例化另一个控制器并执行其操作方法。


这将不会在地址栏中显示所期望的URL。 - arserbin3
1
@arserbin3 - Server.Transfer也不行。这很可能是为什么最初提出这个问题的要求。 - Richard Szalay

2
Server.TransferRequest在MVC中完全不必要。这是一个陈旧的功能,只在ASP.NET中需要,因为请求直接到达页面,需要一种方法来将请求转移到另一个页面。现代版本的ASP.NET(包括MVC)具有可自定义以直接路由到所需资源的路由基础结构。当您可以使请求直接到达所需的控制器和操作时,没有让请求到达控制器然后再将其转移到另一个控制器的意义。

更重要的是,由于您正在响应原始请求,因此没有必要将任何内容放入TempData或其他存储中,仅仅是为了将请求路由到正确的位置。相反,您将带着原始请求到达控制器操作。您还可以放心使用此方法,因为它完全在服务器端执行,Google也会批准这种方法。

虽然您可以从IRouteConstraintIRouteHandler两者中做很多事情,但路由的最强大的扩展点是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存储起来会有意义,这样当他们返回时就可以收到相同的首页版本。
还要注意,使用这种方法,您可以自定义路由以考虑查询字符串参数(默认情况下完全忽略它们),并相应地路由到适当的控制器操作。
其他示例:

1
如果我不想在进入操作时立即进行转移,而是让该操作执行一些工作,然后有条件地转移到另一个操作,怎么办?更改我的路由以直接到达转移目标是行不通的,因此看起来Server.TransferRequest在MVC中并非“完全不必要”。 - ProfK
在某些情况下,我会在 global.asaxApplication_Error 处理程序中使用 Server.TransferRequest 来使我的应用程序返回一个用户友好的错误页面。我不知道这是否是最佳解决方案,但没有找到更好的方法来处理所有错误。 - R. Schreurs

2
你可以创建另一个控制器并调用返回结果的操作方法。但这需要你将视图放入共享文件夹中。
我不确定你所说的“复制”是否指的是这个。
return new HomeController().Index();

编辑

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


这可能是一种方法,但它似乎没有完全正确的上下文 - 即使我说hc.ControllerContext = this.ControllerContext。此外,它会在~/Views/Gateway/5.aspx下查找视图,但找不到它。 - Simon_Weaver
此外,您还会失去所有的Action Filters。您可能想尝试使用控制器必须实现的IController接口上的Execute方法。例如:((IController)new HomeController()).Execute(...)。这样,您仍然可以参与Action Invoker管道。不过,您需要弄清楚要传递什么参数给Execute... Reflector可能会有所帮助 :) - Andrew Stanton-Nurse
是的,我不喜欢新建一个控制器的想法,我认为最好定义自己的控制器工厂,这似乎是正确的扩展点。但是我对这个框架只是略知一二,所以我的看法可能有偏差。 - JoshBerke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接