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
context.Response.Redirect("/Account/ChangePassword");
. This worked but it first executed the original action method requested by the user. This behavior defeated the purpose.HttpContext.SetEndpoint
, but there was no example available to work with.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);
}
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
IViewLocationExpander
USAGE
public static void ChangeDefaultPassword(this HttpContext context)
=> context.RedirectToPath("Account","ChangePassword");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With