views:

3267

answers:

11

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??..

A: 

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.

Richard
its based on programatic conditions. i.e. campaign 100 might go to view 7 and campaign 200 might go to view 8 etc. etc. too complicated for routing
Simon_Weaver
+3  A: 

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();
Brian Sullivan
+3  A: 

Just instance the other controller and execute it's action method.

Richard Szalay
+1  A: 

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.

JoshBerke
this might be the approach, but it doesnt seem to quite have the context right - even if I say hc.ControllerContext = this.ControllerContext. Plus it then looks for the view under ~/Views/Gateway/5.aspx and doesn't find it.
Simon_Weaver
Plus you lose all the Action Filters. You probably want to try using the Execute method on the IController interface that your controllers must implement. For example: ((IController)new HomeController()).Execute(...). That way you still participate in the Action Invoker pipeline. You'd have to figure out exactly what to pass in to Execute though... Reflector might help there :)
anurse
Yep I don't like the idea of newing up a controller, I think your better off defining your own controller factory which seems like the proper extension point for this. But i've barely scratched the surface of this framework so I might be way off.
JoshBerke
A: 

Implementing your own dynamic routing by implementing the IRouteHandler might be a solution. See http://stackoverflow.com/questions/379558/mvc-net-routing/379823#379823 for more details.

Aleris
this would only help with a gateway right - and not the general case
Simon_Weaver
+6  A: 

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);
    }
if this works it looks like what i'm looking for!
Simon_Weaver
+13  A: 

How about a TransferResult class? (based on Stans answer)

/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : RedirectResult
{
    public TransferResult(string url)
        : base(url)
    {
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var httpContext = HttpContext.Current;

        httpContext.RewritePath(Url, false);

        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest(HttpContext.Current);
    }
}
Markus Olsson
this works great. be careful not to end up with an infinite loop - as i did on my first attempt by passing the wrong URL in. i made a small modification to allow a route value collection to be passed in which may be useful to others. posted above or below...
Simon_Weaver
update: this solution seems to work well, and although I am using it only in a very limited capacity haven't yet found any issues
Simon_Weaver
one issue: cannot redirect from POST to GET request - but thats not necessarily a bad thing. something to be cautious of though
Simon_Weaver
im torn as to who to give points to - but need to get my accept ratio higher. thanks stan and markus! please also see my addition to them both : http://stackoverflow.com/questions/799511/how-to-simulate-server-transfer-in-asp-net-mvc/1242525#1242525
Simon_Weaver
+10  A: 

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;

        httpContext.RewritePath(Url, false);

        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest(HttpContext.Current);
    }
}
Simon_Weaver
+3  A: 

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.

Hari
A: 

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!

William
A: 

You can use Server.TransferRequest on IIS7+ instead.

Nitin