Search code examples
.net-coreurl-routingasp.net-core-3.1.net-core-3.1

Changing Request Path in .Net Core 3.1


Prior to 3.0, I could change the path of a request (without any form of browser redirection) by just accessing the HttpRequest property of the HttpContext and then changed the value of the Path.

As an example, to display a page for a user who needed to change his/her password (irrespective of the page the user intended to visit), I extended the HttpContext

public static void ChangeDefaultPassword(this HttpContext context) 
=> context.Request.Path = "/Account/ChangePassword";

This piece of code takes the user to the action method ChangePassword in the AccountController without executing the action method the user intends to visit.

Then enters dotnet core 3.1.

In 3.1, the extension method changes the path. However, it never executes the action method. It ignores the updated path.

I am aware this is due to the changes in the routing.The endpoint can now be accessed with the extension method HttpContext.GetEndpoint(). There is also an extension method HttpContext.SetEndpoint which seems to be the right way to set a new endpoint. However, there is no sample of how to accomplish this.

The Question

How do I change the request path, without executing the original path?

What I Have Tried

  1. I tried changing the path. It seems routing in dotnet core 3.1 ignores the value of the HttpRequest path value.
  2. I tried redirecting with context.Response.Redirect("/Account/ChangePassword");. This worked but it first executed the original action method requested by the user. This behavior defeated the purpose.
  3. I tried using the extension method HttpContext.SetEndpoint, but there was no example available to work with.

Solution

  • I was able to find a working solution. My solution works by manually setting a new endpoint with the SetEndpoint extension method.

    Here is an extension method I created to resolve this issue.

    private static void RedirectToPath(this HttpContext context, string controllerName, string actionName )
    {
        // Get the old endpoint to extract the RequestDelegate
        var currentEndpoint = context.GetEndpoint();
    
        // Get access to the action descriptor collection
        var actionDescriptorsProvider =
            context.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
    
        // Get the controller aqction with the action name and the controller name.
        // You should be redirecting to a GET action method anyways. Anyone can provide a better way of achieving this. 
        var controllerActionDescriptor = actionDescriptorsProvider.ActionDescriptors.Items
            .Where(s => s is ControllerActionDescriptor bb
                        && bb.ActionName == actionName
                        && bb.ControllerName == controllerName
                        && (bb.ActionConstraints == null
                            || (bb.ActionConstraints != null
                                && bb.ActionConstraints.Any(x => x is HttpMethodActionConstraint cc
                                && cc.HttpMethods.Contains(HttpMethods.Get)))))
            .Select(s => s as ControllerActionDescriptor)
            .FirstOrDefault();
    
        if (controllerActionDescriptor is null) throw new Exception($"You were supposed to be redirected to {actionName} but the action descriptor could not be found.");
    
        // Create a new route endpoint
        // The route pattern is not needed but MUST be present. 
        var routeEndpoint = new RouteEndpoint(currentEndpoint.RequestDelegate, RoutePatternFactory.Parse(""), 1, new EndpointMetadataCollection(new object[] { controllerActionDescriptor }), controllerActionDescriptor.DisplayName);
        
        // set the new endpoint. You are assured that the previous endpoint will never execute.
        context.SetEndpoint(routeEndpoint);
    }
    

    Important

    1. You must make the view of the action method available by placing it in the Shared folder. Alternatively, you may decide to provide a custom implementation of IViewLocationExpander
    2. Before accessing the endpoint, the routing middleware must have executed.

    USAGE

    public static void ChangeDefaultPassword(this HttpContext context) 
    => context.RedirectToPath("Account","ChangePassword");