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

问题描述 投票:121回答:14

在ASP.NET MVC中,您可以非常轻松地返回重定向ActionResult:

 return RedirectToAction("Index");

 or

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

这实际上会提供HTTP重定向,这通常很好。但是,当使用谷歌分析时,这会导致很大的问题,因为最初的引用会丢失,所以谷歌不知道你来自哪里。这会丢失有用的信息,例如任何搜索引擎术语。

作为旁注,此方法的优点是可以删除任何可能来自广告系列但仍允许我捕获服务器端的参数。将它们留在查询字符串中会导致人们加入书签或推特或博客链接,而这些链接不应该存在。我已经多次看到这样的情况,人们在我们的网站上链接了包含广告系列ID的链接。

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

现在我更关心谷歌(比意外书签),我希望能够派人访问/,如果他们去了/home/7,这是主页的第7版,他们会得到的页面。

就像我之前说的如果我这样做,我失去了谷歌分析引用者的能力:

 return RedirectToAction(new { controller = "home", version = 7 });

我真正想要的是一个

 return ServerTransferAction(new { controller = "home", version = 7 });

这将使我看到没有客户端重定向的视图。我不认为这样的事情存在。

目前我能想到的最好的事情是在我的HomeController.Index(..) Action中复制GatewayController.Index的整个控制器逻辑。这意味着我必须将'Views/Home'移动到'Shared',以便它可以访问。肯定有更好的办法??..

asp.net-mvc server.transfer
14个回答
129
投票

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's post的代码)。它应该(无法测试)也可以通过查看它是否在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
投票

不路由只是为您处理这种情况?即,对于上述场景,您可以创建一个实现此逻辑的路由处理程序。


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

1
投票

在MVC中完全没有Server.TransferRequest。这是一个过时的功能,只有在ASP.NET中才需要,因为请求直接来到页面,需要有一种方法将请求传输到另一个页面。现代版本的ASP.NET(包括MVC)具有路由基础结构,可以自定义以直接路由到所需的资源。当您只需将请求直接发送到控制器并执行您想要的操作时,没有必要让请求到达控制器只将其传输到另一个控制器。

更重要的是,由于您正在响应原始请求,因此无需将任何内容放入TempData或其他存储中,只是为了将请求路由到正确的位置。相反,您到达控制器操作时原始请求保持不变。您也可以放心,谷歌将批准这种方法,因为它完全在服务器端发生。

虽然你可以从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,因此当他们返回时,他们会收到相同的主页版本。

另请注意,使用此方法可以自定义路由以将查询字符串参数考虑在内(默认情况下完全忽略它们)并相应地路由到相应的控制器操作。

Additional Examples


0
投票

本身不是答案,但显然要求不仅仅是实际导航“做”Webforms Server.Transfer()的等效功能,而且还要在单元测试中完全支持所有这些。

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

我正在考虑通过查看Reflector,做任何RedirectToRouteResult类以及各种Controller基类方法来做到这一点,然后通过扩展方法将后者“添加”到Controller。也许这些可能是同一类中的静态方法,以便于下载的轻松/懒惰?

如果我这样做,我会发布它,否则也许别人可能会打败我!


0
投票

我通过在视图中利用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");
}

47
投票

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

如果您使用的是IIS7,则以下修改似乎适用于ASP.NET MVC 3.感谢@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);
    }
}

13
投票

您可以在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();

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


6
投票

而不是模拟服务器传输,MVC仍然能够实际执行Server.TransferRequest

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

5
投票

只是实例化另一个控制器并执行它的动作方法。


2
投票

您可以新建另一个控制器并调用返回结果的操作方法。这将要求您将视图放入共享文件夹。

我不确定这是不是你的意思,但是:

return new HomeController().Index();

编辑

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

© www.soinside.com 2019 - 2024. All rights reserved.