Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
like image 494
Shittu Joseph Olugbenga Avatar asked Feb 04 '20 21:02

Shittu Joseph Olugbenga


2 Answers

The way I worked around this issue is to use EndpointDataSource directly, which is a singleton service that is available from DI as long as you have the routing services registered. It works as long as you can provide the controller name and the action name, which you can specify at compile time. This negates the need to use IActionDescriptorCollectionProvider or build the endpoint object or request delegate by yourself (which is pretty complicated...):

public static void RerouteToActionMethod(this HttpContext context, EndpointDataSource endpointDataSource, string controllerName, string actionName)
{
    var endpoint = endpointDataSource.Endpoints.FirstOrDefault(e =>
    {
        var descriptor = e.Metadata.GetMetadata<ControllerActionDescriptor>();
        // you can add more constraints if you wish, e.g. based on HTTP method, etc
        return descriptor != null
               && actionName.Equals(descriptor.ActionName, StringComparison.OrdinalIgnoreCase)
               && controllerName.Equals(descriptor.ControllerName, StringComparison.OrdinalIgnoreCase);
    });

    if (endpoint == null)
    {
        throw new Exception("No valid endpoint found.");
    }

    context.SetEndpoint(endpoint);
}
like image 148
scharnyw Avatar answered Sep 30 '22 05:09

scharnyw


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");
like image 45
Shittu Joseph Olugbenga Avatar answered Sep 30 '22 06:09

Shittu Joseph Olugbenga