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:

Image of the pipeline for requests /app1/some/path and /myapp/something/else

As shown in the diagram, the rules for PathBase in this situation are:

  • The PathBaseMiddleware moves the specified prefix from Path to PathBase if the request has the prefix. If the request does not have the prefix, the PathBaseMiddleware does nothing.
  • When a request takes a Map() branch in the pipeline, it moves the prefix from Path to PathBase

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, called api1, which prints the path information and a link to api2
  • /api2, called api2, which prints the path information and a link to api1
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:

Image of the pipeline with routing and endpoints

If we run this app and test each API, with and without the prefix, you get the following results

  • /api1API1: PathBase: Path: /api1 Link: /api2
  • /api2API2: PathBase: Path: /api2 Link: /api1
  • /myapp/api1API1: PathBase: /myapp Path: /api1 Link: /myapp/api2
  • /myapp/api2API2: 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)}")
}
  • /api1API1: PathBase: Path: /api1 Link: /api2
  • /api2API2: PathBase: Path: /api2 Link: /api1
  • /myapp/api1FALLBACK: PathBase: /myapp Path: /api1 Link: /myapp/api1
  • /myapp/api2FALLBACK: 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:

Image showing routing not working as you might expect when placed before PathBase

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.