问题
In ASP.NET MVC you can return a redirect ActionResult quite easily :
return RedirectToAction(\"Index\");
or
return RedirectToRoute(new { controller = \"home\", version = Math.Random() * 10 });
This will actually give an HTTP redirect, which is normally fine. However, when using google analytics this causes big issues because the original referer is lost so google doesnt know where you came from. This loses useful information such as any search engine terms.
As a side note, this method has the advantage of removing any parameters that may have come from campaigns but still allows me to capture them server side. Leaving them in the query string leads to people bookmarking or twitter or blog a link that they shouldn\'t. I\'ve seen this several times where people have twittered links to our site containing campaign IDs.
Anyway, I am writing a \'gateway\' controller for all incoming visits to the site which i may redirect to different places or alternative versions.
For now I care more about Google for now (than accidental bookmarking), and I want to be able to send someone who visits /
to the page that they would get if they went to /home/7
, which is version 7 of a homepage.
Like I said before If I do this I lose the ability for google to analyse the referer :
return RedirectToAction(new { controller = \"home\", version = 7 });
What i really want is a
return ServerTransferAction(new { controller = \"home\", version = 7 });
which will get me that view without a client side redirect. I don\'t think such a thing exists though.
Currently the best thing I can come up with is to duplicate the whole controller logic for HomeController.Index(..)
in my GatewayController.Index
Action. This means I had to move \'Views/Home\'
into \'Shared\'
so it was accessible. There must be a better way??..
回答1:
How about a TransferResult class? (based on 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);
}
}
}
Updated: Now works with MVC3 (using code from Simon's post). It should (haven't been able to test it) also work in MVC2 by looking at whether or not it's running within the integrated pipeline of IIS7+.
For full transparency; In our production environment we've never use the TransferResult directly. We use a TransferToRouteResult which in turn calls executes the TransferResult. Here's what's actually running on my production servers.
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);
}
}
And if you're using T4MVC (if not... do!) this extension might come in handy.
public static class ControllerExtensions
{
public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
{
return new TransferToRouteResult(result.GetRouteValueDictionary());
}
}
Using this little gem you can do
// in an action method
TransferToAction(MVC.Error.Index());
回答2:
Edit: Updated to be compatible with ASP.NET MVC 3
Provided you are using IIS7 the following modification seems to work for ASP.NET MVC 3. Thanks to @nitin and @andy for pointing out the original code didn't work.
Edit 4/11/2011: TempData breaks with Server.TransferRequest as of MVC 3 RTM
Modified the code below to throw an exception - but no other solution at this time.
Here's my modification based upon Markus's modifed version of Stan's original post. I added an additional constructor to take a Route Value dictionary - and renamed it MVCTransferResult to avoid confusion that it might just be a redirect.
I can now do the following for a redirect:
return new MVCTransferResult(new {controller = "home", action = "something" });
My modified class :
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:
You can use Server.TransferRequest on IIS7+ instead.
回答4:
I found out recently that ASP.NET MVC doesn't support Server.Transfer() so I've created a stub method (inspired by 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:
Couldn't you just create an instance of the controller you would like to redirect to, invoke the action method you want, then return the result of that? Something like:
HomeController controller = new HomeController();
return controller.Index();
回答6:
I wanted to re-route the current request to another controller/action, while keeping the execution path exactly the same as if that second controller/action was requested. In my case, Server.Request wouldn't work because I wanted to add more data. This is actually equivalent the current handler executing another HTTP GET/POST, then streaming the results to the client. I'm sure there will be better ways to achieve this, but here's what works for me:
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);
Your guess is right: I put this code in
public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter
and I'm using it to display errors to developers, while it'll be using a regular redirect in production. Note that I didn't want to use ASP.NET session, database, or some other ways to pass exception data between requests.
回答7:
Rather than simulate a server transfer, MVC is still capable of actually doing a Server.TransferRequest:
public ActionResult Whatever()
{
string url = //...
Request.RequestContext.HttpContext.Server.TransferRequest(url);
return Content("success");//Doesn't actually get returned
}
回答8:
Just instance the other controller and execute it's action method.
回答9:
You could new up the other controller and invoke the action method returning the result. This will require you to place your view into the shared folder however.
I'm not sure if this is what you meant by duplicate but:
return new HomeController().Index();
Edit
Another option might be to create your own ControllerFactory, this way you can determine which controller to create.
回答10:
Doesn't routing just take care of this scenario for you? i.e. for the scenario described above, you could just create a route handler that implemented this logic.
回答11:
For anyone using expression-based routing, using only the TransferResult class above, here's a controller extension method that does the trick and preserves TempData. No need for 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:
Server.TransferRequest
is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
What's more is that since you are responding to the original request, there is no need to tuck anything into TempData
or other storage just for the sake of routing the request to the right place. Instead, you arrive at the controller action with the original request intact. You also can be rest assured that Google will approve of this approach as it happens entirely on the server side.
While you can do quite a bit from both IRouteConstraint
and IRouteHandler
, the most powerful extension point for routing is the RouteBase
subclass. This class can be extended to provide both incoming routes and outgoing URL generation, which makes it a one stop shop for everything having to do with the URL and the action that URL executes.
So, to follow your second example, to get from /
to /home/7
, you simply need a route that adds the appropriate route values.
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 }
);
}
}
But going back to your original example where you have a random page, it is more complex because the route parameters cannot change at runtime. So, it could be done with a RouteBase
subclass as follows.
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;
}
}
Which can be registered in routing like:
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 }
);
}
}
Note in the above example, it might make sense to also store a cookie recording the home page version the user came in on so when they return they receive the same home page version.
Note also that using this approach you can customize routing to take query string parameters into consideration (it completely ignores them by default) and route to an appropriate controller action accordingly.
Additional Examples
- https://stackoverflow.com/a/31958586
- https://stackoverflow.com/a/36774498
- https://stackoverflow.com/a/36168395
回答13:
Not an answer per se, but clearly the requirement would be not only for the actual navigation to "do" the equivalent functionality of Webforms Server.Transfer(), but also for all of this to be fully supported within unit testing.
Therefore the ServerTransferResult should "look" like a RedirectToRouteResult, and be as similar as possible in terms of the class hierarchy.
I'm thinking of doing this by looking at Reflector, and doing whatever RedirectToRouteResult class and also the various Controller base class methods do, and then "adding" the latter to the Controller via extension methods. Maybe these could be static methods within the same class, for ease/laziness of downloading?
If I get round to doing this I'll post it up, otherwise maybe somebody else might beat me to it!
回答14:
I achieved this by harnessing the Html.RenderAction
helper in a View:
@{
string action = ViewBag.ActionName;
string controller = ViewBag.ControllerName;
object routeValues = ViewBag.RouteValues;
Html.RenderAction(action, controller, routeValues);
}
And in my controller:
public ActionResult MyAction(....)
{
var routeValues = HttpContext.Request.RequestContext.RouteData.Values;
ViewBag.ActionName = "myaction";
ViewBag.ControllerName = "mycontroller";
ViewBag.RouteValues = routeValues;
return PartialView("_AjaxRedirect");
}
来源:https://stackoverflow.com/questions/799511/how-to-simulate-server-transfer-in-asp-net-mvc