How to redirect MVC action without returning 301? (using MVC 4 beta)
Asked Answered
C

2

7

I'm working on an ASP.NET MVC solution that has a number of different menus. The menu to display depends on the role of the currently logged in user.

In MVC 3 I had some custom code to support this scenario, by having a single controller method that would return the right menu. It would do this by deferring the request to the appropriate controller and action depending on the current user.

This code appears to be broken in MVC 4 and I'm looking for help to fix it.

First, I added a TransferResult helper class to perform the redirection:

public class TransferResult : RedirectResult
{
    #region Transfer to URL
    public TransferResult( string url ) : base( url )
    {
    }
    #endregion

    #region Transfer using RouteValues
    public TransferResult( object routeValues ) : base( GetRouteUrl( routeValues ) )
    {
    }

    private static string GetRouteUrl( object routeValues )
    {
        var url = new UrlHelper( new RequestContext( new HttpContextWrapper( HttpContext.Current ), new RouteData() ), RouteTable.Routes );
        return url.RouteUrl( routeValues );
    }
    #endregion

    #region Transfer using ActionResult (T4MVC only)
    public TransferResult( ActionResult result ) : base( GetRouteUrl( result.GetT4MVCResult() ) )
    {
    }

    private static string GetRouteUrl( IT4MVCActionResult result )
    {
        var url = new UrlHelper( new RequestContext( new HttpContextWrapper( HttpContext.Current ), new RouteData() ), RouteTable.Routes );
        return url.RouteUrl( result.RouteValueDictionary );
    }
    #endregion

    public override void ExecuteResult( ControllerContext context )
    {
        HttpContext httpContext = HttpContext.Current;
        httpContext.RewritePath( Url, false );
        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest( HttpContext.Current );
    }
}

Second, I modified T4MVC to emit a few controller helper methods, resulting in every controller having this method:

protected TransferResult Transfer( ActionResult result )
{
    return new TransferResult( result );
}

This allowed me to have a shared controller action to return a menu, without having to clutter the views with any conditional logic:

public virtual ActionResult Menu()
{
    if( Principal.IsInRole( Roles.Administrator ) )
        return Transfer( MVC.Admin.Actions.Menu() );
    return View( MVC.Home.Views.Partials.Menu );
}

However, the code in ExecuteResult in the TransferResult class does not seem to work with the current preview release of MVC 4. It gives me the following error (pointing to the "httpHandler.ProcessRequest" line):

'HttpContext.SetSessionStateBehavior' can only be invoked before
'HttpApplication.AcquireRequestState' event is raised.

Any idea how to fix this?

PS: I realize that I could achieve the same using a simple HtmlHelper extension, which is what I'm currently using as a workaround. However, I have many other scenarios where this method has allowed me to mix and reuse actions, and I would hate to give up this flexibility when moving to MVC 4.

Cohn answered 4/3, 2012 at 22:57 Comment(3)
You should put that method in a base Controller class, not in every controller.Urinalysis
@Urinalysis That seems to be a micro-optimization given that the code is generated by T4MVC and it's just a one-liner. Also doesn't do anything to help solve the problem ;)Cohn
Must be a slow day; can't even get an upvote :o)Cohn
M
5

Sometimes I think "MVC" should be called "RCMV" for "Router Controller Model View" since that is really the order that things happen. Also, since it is just "MVC", people always tend to forget about routing. The great thing about MVC is that routing configurable and extensible. I believe what you are trying to do could be solved with a custom route handler.

I haven't tested this, but you should be able to do something like this:

routes.Add(
   new Route(
       "{controller}/{action}/{id}", 
       new RouteValueDictionary(new { controller = "Home", action = "Menu" }), 
       new MyRouteHandler(Roles.Administrator, new { controller = "Admin" })));

Then your route handler would look like this:

public class MyRouteHandler : IRouteHandler
{
    public string Role { get; set; }

    public object RouteValues { get; set; }

    public MyRouteHandler(string role, object routeValues)
    {
        Role = role;
        RouteValues = routeValues;
    }

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MyHttpHandler(Role, RouteValues);
    }
}

And finally handle the re-routing in your HttpHandler:

public class MyHttpHandler : IHttpHandler
{
    public string Role { get; set; }

    public object RouteValues { get; set; }

    public MyHttpHandler(string role, object routeValues)
    {
        Role = role;
        RouteValues = routeValues;
    }

    public void ProcessRequest(HttpContext httpContext)
    {
        if (httpContext.User.IsInRole(Role))
        {
            RouteValueDictionary routeValues = new RouteValueDictionary(RouteValues);

            // put logic here to create path similar to what you were doing
            // before but you will need to replace any keys in your route 
            // with the values from the dictionary created above.

            httpContext.RewritePath(path);
        }

        IHttpHandler handler = new MvcHttpHandler();
        handler.ProcessRequest(httpContext);
    }
}

That may not be 100% correct, but it should get you in the right direction in a way that shouldn't run into anything deprecated in MVC4.

Mier answered 14/3, 2012 at 1:30 Comment(3)
+1 for coming up with a solution to the problem. However, the old code did provide a single shared solution that I could use throughout my controller methods as needed, whereas this requires custom code for every use. I'd prefer a more generic solution if one exists.Cohn
Updated answer to be more generic. Another advantage of this solution is that it can be put in a reusable library (assuming your role names can also be put in that library) and then you can use it across multiple MVC apps. Your static method was internal to your app.Mier
With the old solution I had a class library with the shared bits and a modified T4MVC.tt template to generate the controller methods, but I'd always be including that anyway. As far as I can tell, this still only works for routes that I manually configure to enable this override by specifying the custom handler. Or am I missing something?Cohn
C
1

I think a TransferResult should be included in the framework without each developer wrestling with having to reimplement it for different versions when it becomes broken. (as in this thread and for example the following thread too: Implementing TransferResult in MVC 3 RC - does not work ).

If you agree with me, I would just like to encourage you to vote for "Server.Transfer" to become included in the MVC framework itself: http://aspnetwebstack.codeplex.com/workitem/798

Confide answered 24/1, 2013 at 14:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.