How to simulate Server.Transfer in ASP.NET MVC?
Asked Answered
A

14

127

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.

Analphabetic answered 28/4, 2009 at 19:34 Comment(5)
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)Bakelite
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.Analphabetic
"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 for details.Hispaniola
@Hispaniola 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.Analphabetic
@Analphabetic - 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?Hispaniola
B
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());
Barthold answered 25/7, 2009 at 0:3 Comment(13)
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...Analphabetic
update: this solution seems to work well, and although I am using it only in a very limited capacity haven't yet found any issuesAnalphabetic
one issue: cannot redirect from POST to GET request - but thats not necessarily a bad thing. something to be cautious of thoughAnalphabetic
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 : #800011Analphabetic
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!Analphabetic
Uhm, what extensions namespace is missing? this.RouteName and this.RouteValues do not exist. Using MVC 3 and razor. Error is in TransferResult classTahoe
@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.Kneepad
@BradLaney: You can just remove the 'var urlHelper...' and 'var url...' lines and replace 'url' with 'this.Url' for the rest and it works. :)Kneepad
Is it me or it need a virtual path to work? So I can't transfert to an other domain?Doble
does this work for different domains? I'm getting a Virtual Path error?Atmosphere
var httpContext = HttpContext.Current; is an HORRROOORR !!!!! Use context.HttpContext, never use HttpContext.Current in asp.net mvc !!!!!!Strove
@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?Lixivium
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.Strove
A
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);
    }
}
Analphabetic answered 7/8, 2009 at 1:57 Comment(6)
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.Antineutrino
i haven't yet had a change to look at MVC3. let me know if you find a solutionAnalphabetic
Does Server.TransferRquest as suggested by Nitin do what the above is trying to do?Is
Why do we need to check TempData for null and count > 0?Recognize
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 disappearsAnalphabetic
Looks like .Count is a property not a method, so no ().Unlikelihood
T
15

You can use Server.TransferRequest on IIS7+ instead.

Tradesman answered 17/9, 2010 at 21:9 Comment(0)
G
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);
    }
Grotesquery answered 21/5, 2009 at 11:50 Comment(0)
H
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();
Hibiscus answered 28/4, 2009 at 19:42 Comment(2)
No, the controller you create won't have things like Request and Response setup correctly on it. That can lead to problems.Fenske
I agree with @JeffWalkerCodeRanger: the same thing also after set the property otherController.ControllerContext = this.ControllerContext;Coloring
G
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
}
Gillyflower answered 3/1, 2014 at 21:49 Comment(2)
Feel free to add some text to your answer to explain it further.Unmoor
Note this requires MVCv3 and above.Fleury
D
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.

Danelledanete answered 18/10, 2009 at 10:43 Comment(0)
B
5

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

Bionomics answered 28/4, 2009 at 19:42 Comment(2)
This will not show the desired URL in the address barDieter
@Dieter - Neither will Server.Transfer. This requirement is presumably why the original question was even posted.Bionomics
N
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.

Nielson answered 28/4, 2009 at 19:47 Comment(3)
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.Analphabetic
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 :)Priggery
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.Nielson
H
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

Hispaniola answered 20/1, 2018 at 14:59 Comment(2)
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".Euhemerize
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.Untaught
S
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.

Something answered 28/4, 2009 at 19:40 Comment(2)
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 routingAnalphabetic
Why is that too complicated for routing? What's wrong with custom route constraints? stephenwalther.com/blog/archive/2008/08/07/…Fullmouthed
G
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);
}
Gentilesse answered 18/10, 2012 at 17:43 Comment(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 AppFabricAnalphabetic
I
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!

Inadvertency answered 29/12, 2009 at 17:15 Comment(0)
Z
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");
}
Zeal answered 6/3, 2018 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.