126

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 referrer is lost, so Google doesn't 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 referrer:

 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.

Null
  • 1,950
  • 9
  • 30
  • 33
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • What exactly is a `ServerTransferAction` that you were trying to replicate? Is that an actual thing? (couldn't find any info on it... thanks for the question, btw, the answer below is superb) – jleach Mar 12 '16 at 20:18
  • 1
    Look up Server.Transfer(...). It's a way to basically do a 'redirect' on the server side where the client receives the redirected page without a client side redirect. Generally it's not recommended with modern routing. – Simon_Weaver Mar 12 '16 at 20:22
  • 1
    "Transferring" is an antiquated ASP.NET feature that is no longer necessary in MVC due to the ability to *go directly* to the correct controller action using routing. See [this answer](https://stackoverflow.com/a/48357564/) for details. – NightOwl888 Jan 20 '18 at 15:02
  • @NightOwl888 yes definitely - but also sometimes due to business logic it's necessary/easier. I looked back to see where I'd ended up using this - (fortunately it was only in one place) - where I have a homepage that I wanted to be dynamic for certain complex conditions and so behind the scenes it shows a different route. Definitely want to avoid it as much as possible in favor of routing or route conditions - but sometimes a simple `if` statement is just too tempting a solution. – Simon_Weaver Jan 23 '18 at 21:31
  • @Simon_Weaver - And what is wrong with subclassing `RouteBase` so you can put your `if` statement there instead of bending everything over backwards to jump from one controller to another? – NightOwl888 Jan 23 '18 at 21:47

14 Answers14

132

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());
Community
  • 1
  • 1
Markus Olsson
  • 22,402
  • 9
  • 55
  • 62
  • 1
    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 Aug 07 '09 at 01:54
  • 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 Sep 04 '09 at 01:03
  • one issue: cannot redirect from POST to GET request - but thats not necessarily a bad thing. something to be cautious of though – Simon_Weaver Nov 11 '09 at 01:50
  • 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 Dec 08 '09 at 01:45
  • 1
    Warning: Server.TransferRequest doesn't work with TempData. Application_Error gets hit although at runtime you won't see anything wrong (except TempData will come across empty). See my answer for details. Sorry these list of answers is getting quite convoluted! – Simon_Weaver Apr 12 '11 at 06:25
  • Uhm, what extensions namespace is missing? this.RouteName and this.RouteValues do not exist. Using MVC 3 and razor. Error is in TransferResult class – BradLaney Aug 09 '11 at 19:49
  • @BradLanye: The extension method requires you to have the T4MVC template in your web application root directory and the static class containing the extension needs to reside in the same project or have a project reference to the web application. – Michael Ulmann Sep 01 '11 at 23:18
  • 2
    @BradLaney: You can just remove the 'var urlHelper...' and 'var url...' lines and replace 'url' with 'this.Url' for the rest and it works. :) – Michael Ulmann Sep 02 '11 at 01:39
  • Is it me or it need a virtual path to work? So I can't transfert to an other domain? – VinnyG Sep 28 '11 at 20:46
  • does this work for different domains? I'm getting a Virtual Path error? – andy Mar 27 '12 at 23:22
  • var httpContext = HttpContext.Current; is an HORRROOORR !!!!! Use context.HttpContext, never use HttpContext.Current in asp.net mvc !!!!!! – Softlion Nov 08 '12 at 17:27
  • @Softlion, not trying to be nit-picky but for educations sake, some clarification on why that is so horrendous would greatly add to the discussion. Is your concern coupling? Performance? Portability? Multiple hosting environments? A general disgust for ASP.NET internals? – Norman H Jan 03 '13 at 13:30
  • 1
    1: coupling/unit testing/future compatibility. 2: mvc core/mvc samples never use this singleton. 3: this singleton is not available in a thread (null), either a pool thread or an async delegate called on a context other than default, like when using async action methods. 4: for compatibility purposes only, mvc sets this singleton value to context.HttpContext before entering user code. – Softlion Jan 03 '13 at 17:57
48

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);
    }
}
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    This seems not to be working in MVC 3 RC. Fails on HttpHandler.ProcessRequest(), says: 'HttpContext.SetSessionStateBehavior' can only be invoked before 'HttpApplication.AcquireRequestState' event is raised. – Andy Nov 13 '10 at 20:50
  • i haven't yet had a change to look at MVC3. let me know if you find a solution – Simon_Weaver Nov 15 '10 at 09:03
  • Does Server.TransferRquest as suggested by Nitin do what the above is trying to do? – Old Geezer Nov 03 '12 at 04:36
  • Why do we need to check TempData for null and count > 0? – yurart Feb 28 '17 at 14:08
  • You don't, but its just a safety feature so if you are already using it and relying on it then you won't be left scratching your head if it disappears – Simon_Weaver Mar 01 '17 at 01:13
  • Looks like .Count is a property not a method, so no (). – Dronz Jul 24 '18 at 00:14
15

You can use Server.TransferRequest on IIS7+ instead.

Nitin Agarwal
  • 3,830
  • 1
  • 21
  • 12
12

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

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
}
AaronLS
  • 37,329
  • 20
  • 143
  • 202
9

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
  • 27,513
  • 23
  • 77
  • 91
  • 6
    No, the controller you create won't have things like Request and Response setup correctly on it. That can lead to problems. – Jeff Walker Code Ranger Jun 05 '14 at 19:00
  • 1
    I agree with @JeffWalkerCodeRanger: the same thing also after set the property `otherController.ControllerContext = this.ControllerContext;` – T-moty Jun 19 '15 at 15:13
7

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.

5

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

Richard Szalay
  • 83,269
  • 19
  • 178
  • 237
2

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

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • 1
    What if I don't want to transfer immediately on entering an action, but rather let that action do some work and then conditionally transfer to another action. Changing my routing to go directly to the transfer target won't work, so it looks like `Server.TransferRequest` is not, after all, "completely unnecessary in MVC". – ProfK Jan 29 '20 at 10:51
  • In some cases I use `Server.TransferRequest` in the `Application_Error` handler of `global.asax` to make my application return a user-friendly error page. I do not know if that is the best solutio, but didn't find a better way for catch-all error handling. – R. Schreurs Apr 13 '23 at 08:00
2

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
  • 66,142
  • 25
  • 126
  • 164
  • 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 Apr 28 '09 at 19:57
  • 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 :) – Andrew Stanton-Nurse Apr 29 '09 at 17:44
  • 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 Apr 29 '09 at 17:50
1

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);
}
  • 1
    Warning: this seems to cause an error 'The SessionStateTempDataProvider class requires session state to be enabled' although it actually still works. I only see this error in my logs. I'm using ELMAH for error logging and get this error for InProc and AppFabric – Simon_Weaver May 12 '13 at 01:44
1

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
  • 1,169
  • 6
  • 8
  • 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 Apr 28 '09 at 19:59
  • 4
    Why is that too complicated for routing? What's wrong with custom route constraints? http://stephenwalther.com/blog/archive/2008/08/07/asp-net-mvc-tip-30-create-custom-route-constraints.aspx – Ian Mercer Nov 14 '10 at 00:16
0

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
  • 231
  • 2
  • 2
0

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");
}
Colin
  • 22,328
  • 17
  • 103
  • 197