How to redirect MVC action without returning 301?

2019-04-24 18:30发布

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.

2条回答
Evening l夕情丶
2楼-- · 2019-04-24 19:01

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.

查看更多
Evening l夕情丶
3楼-- · 2019-04-24 19:01

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

查看更多
登录 后发表回答