
In this post I'll describe a lesser-known property on HttpRequest called PathBase
. I describe what it does, when it's useful, and how to use it.
What is PathBase?
PathBase
is a property on the HttpRequest
object in ASP.NET Core, and is similar to Path
. PathBase
contains part of the original HTTP request's path, while Path
contains the remainder. As an HttpRequest
moves through the ASP.NET Core middleware pipeline, middleware can move segments from the original HttpRequest.Path
property to HttpRequest.PathBase
. This can be useful in some routing and link generation scenarios.
That's all a bit abstract, so let's look at an example. Let's say you have a middleware pipeline that consists of the PathBaseMiddleware
(more on this later), a Map
"side-branch", and some terminal middleware (which always return a response describing the Path
and PathBase
values)
Don't be confused by the
Map()
function; this isn't minimal APIs, this is pure ASP.NET Core middleware branching. I'm deliberately avoiding the routing system at the moment, we'll add it in again shortly!
In code this looks something like:
public void Configure(IApplicationBuilder app)
{
app.UsePathBase("/myapp");
// Create a "side branch" that always prints the path info when run
app.Map("/app1", app1 => app1
.Run(ctx => ctx.Response.WriteAsync(
$"App1: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path}")));
// If the side branch isn't run, print the path info
app.Run(ctx => ctx.Response.WriteAsync(
$"App1: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path}"));
}
The image below attempts to visualize it, and also follows the progress of 2 requests:
As shown in the diagram, the rules for PathBase
in this situation are:
- The
PathBaseMiddleware
moves the specified prefix fromPath
toPathBase
if the request has the prefix. If the request does not have the prefix, thePathBaseMiddleware
does nothing. - When a request takes a
Map()
branch in the pipeline, it moves the prefix fromPath
toPathBase
Branching middleware pipelines can be very useful, but I haven't seen it used a huge amount these days outside of multi-tenant or specialised scenarios.
I also don't see the PathBaseMiddleware
mentioned much, but I have found it useful in the past.
Using the PathBaseMiddleware
to strip off a prefix
So the purpose of the PathBaseMiddleware
is to remove an optional prefix from a request. When would this be useful?
The scenario I have run into is when you are deploying applications behind some sort of proxy. There are lots of ways this could apply, but lets say, for example, you're deploying your application to Kubernetes, and have an ingress. Requests that start with /myapp1
are routed to your app, while requests that start with /myapp2
are routed to a different app.
With that setup, every request sent to your application will have the /myapp1
prefix. But you don't want to have to add the /myapp1
prefix to all the links in your application. And what if the proxy configuration changes, and you instead need to host your application behind /some-other-path
; you don't want all your links to break.
I realise that most proxies will allow you to strip off the prefix as part of the routing, but I'm ignoring that case for now!
PathBase
provides a mechanism to do this in one place across your app. When generating links with LinkGenerator
, it will take the PathBase
into account, but for routing purposes it will only look at the Path
part.
For an example of this, consider the following minimal API setup. This creates 2 endpoints
/api1
, calledapi1
, which prints the path information and a link toapi2
/api2
, calledapi2
, which prints the path information and a link toapi1
public void Configure(IApplicationBuilder app)
{
app.UsePathBase("/myapp");
app.UseRouting();
app.MapGet("/api1", (HttpContext ctx, LinkGenerator link)
=> $"API1: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path} Link: {link.GetPathByName(ctx, "api2", values: null)}")
.WithName("api1");
app.MapGet("/api2", (HttpContext ctx, LinkGenerator link)
=> $"API2: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path} Link: {link.GetPathByName(ctx, "api1", values: null)}")
.WithName("api2");
}
This builds a pipeline that looks something like the following:
If we run this app and test each API, with and without the prefix, you get the following results
/api1
—API1: PathBase: Path: /api1 Link: /api2
/api2
—API2: PathBase: Path: /api2 Link: /api1
/myapp/api1
—API1: PathBase: /myapp Path: /api1 Link: /myapp/api2
/myapp/api2
—API2: PathBase: /myapp Path: /api2 Link: /myapp/api1
Note that in both cases, the Path
at the point the UseRouting()
middleware is executed is the same, so routing works the same both times. But we're not losing that prefix information, so LinkGenerator
can use it to generate links to the correct place in your application.
Placing UsePathBase()
in the correct location
You should note that in the above example I placed the UseRouting()
call after the call to UsePathBase()
. That was important. If we swap those two calls in the middleware pipeline, then the behaviour is very different. To demonstrate that I'm going to add a third endpoint, a fallback, which is called if no other endpoint executes:
public void Configure(IApplicationBuilder app)
{
// ⚠ NOTE this is generally the wrong order
app.UseRouting();
app.UsePathBase("/myapp");
app.MapGet("/api1", (HttpContext ctx, LinkGenerator link)
=> $"API1: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path} Link: {link.GetPathByName(ctx, "api2", values: null)}")
.WithName("api1");
app.MapGet("/api2", (HttpContext ctx, LinkGenerator link)
=> $"API2: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path} Link: {link.GetPathByName(ctx, "api1", values: null)}")
.WithName("api2");
app.MapFallback((HttpContext ctx, LinkGenerator link)
=> $"FALLBACK: PathBase: {ctx.Request.PathBase} Path: {ctx.Request.Path} Link: {link.GetPathByName(ctx, "api1", values: null)}")
}
/api1
—API1: PathBase: Path: /api1 Link: /api2
✅/api2
—API2: PathBase: Path: /api2 Link: /api1
✅/myapp/api1
—FALLBACK: PathBase: /myapp Path: /api1 Link: /myapp/api1
❌/myapp/api2
—FALLBACK: PathBase: /myapp Path: /api1 Link: /myapp/api1
❌
The first two requests, in which we don't provide the /myapp
prefix, are routed to the correct endpoint, just as before. If the UsePathBase()
prefix isn't provided, it's a noop, so this is as expected.
However, requests which do include the PathBase
prefix aren't routed correctly. They are both falling back to the fallback endpoint. However, you can see in the fallback response that the PathBase
and Path
values are as we would expect. They're the same as in the previous case when UseRouting()
was placed after UsePathBase()
, so why isn't it routing correctly? The following diagram tries to demonstrate the issue:
The problem is that when the request reaches the UseRouting()
endpoint, it attempts to match a route based on the entire path at that point. When we place UseRouting()
first, we don't remove the /myapp
prefix, so we're attempting to route based on the entire /myapp/api1
path, which fails.
However, when the request continues through the middleware pipeline, it then encounters the PathBaseMiddleware
, which moves the /myapp
prefix into PathBase
. But by that point, it's too late for the routing!
Based on these examples, you will generally want to place the call to UsePathBase()
before your call to UseRouting()
. Where that gets tricky is if you're using the new WebApplication
builder with minimal hosting in .NET 6. As I described in my previous post, WebApplication
adds an implicit call to UseRouting()
, so you have to be careful about when you call UsePathBase()
. I'll explore this in the next post, showing the problem, and some ways to work around it.