Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Resolving Paths To Server Relative Paths in .NET Code


:P
On this page:

Path Resolution Banner

ASP.NET Core automatically resolves 'virtual' paths in Razor pages or views to a site relative path. So if you have something like:

<script src="~/lib/scripts/helpers.js" />

It will resolve to the appropriate root Url of the site. In most cases that will be a root path:

/lib/scripts/helpers

But if you're running the Web site out of a virtual folder you may have a folder like /docs/ as the root for the site in which case the above resolves to:

/docs/lib/scripts/helpers

In other words, ~/ resolves the path to the site root.

There's also IUrlHelper which is provided as an Url property on Controllers and in Razor Views and Pages. It gives you programmatic access to the same functionality there via the Content() method.

The problem with both of these is that they are tied to an ActionContext so you need a Controller or Razor Context to use it. If you are in those environments - great, that works just fine. But if you need to access this functionality in Middleware, an API or elsewhere deeper in the request pipeline or even in your business or custom rendering layer, there's no direct support for this functionality.

Url.Content() is also limited to resolving the ~/ expression. I personally also prefer to resolve any root and relative Urls to their site relative Urls which is not supported by the Content() method.

Why even resolve Urls?

Url resolution is not something you need to do often. Mainly because the vast majority of sites run on the root folder (/). If you know that's always the case you can skip the whole ~/ bit and just use / instead.

<script src="/lib/scripts/helpers.js" />

But that can get you in trouble if for some reason in the future the site no longer runs on root, but moves to a sub folder like /docs/. Now all those / links point to the wrong site, the root site rather than the docs site resulting in broken links.

It's also an issue if you need to resolve parts dynamically as part of some business logic that perhaps needs to dynamically create links in documents. An example, where I'm running into this is fixing up documentation links to other topics and ensuring those links are referenced properly regardless of root path because the resulting documentation can be published anywhere - including in site sub folders.

Long story short, there are situations where IUrlHelper doesn't work or could use some additional functionality.

Getting the Path Base

The good news is that as long as you're inside of a running HttpContext and HttpRequest you can easily retrieve the site base path based on the current request with:

string siteBasePath = HttpContext.Request.PathBase.Value;

This gives you either "" for the root or "docs" a /docs/ folder or "docs/class-reference" for /docs/class-reference/ in host scenarios.

For simple scenarios you can replace a path like ~/somepage, by replacing ~/ with $"{siteBasePath}/" for example.

Creating a ResolveUrl() Helper

To make this a bit more generic and to also add additional functionality to also allow fixing up site root paths (ie. /) and relative paths, I've created helper method that resolves Urls more generically.

There are two versions:

  • An extension Method for HttpContext
    This version can take advantage of the available request information to retrieve the current URL, the Host and the PathBase without explicitly specifying anything.

  • A Core String Based Helper Method
    This version does the same thing as the extension method but requires that some of the path components are passed in. This allows using this functionality from anywhere assuming you have access to the host Path and Host information (perhaps from separate configuration) in some form. This is useful if you do custom templating for example or if you need to resolve paths from an API or from within business logic where you don't want to share ASP.NET semantics.

An HttpContext.ResolveUrl() Extension Method

Let's star with the Context version which likely is the more common use case.

This code requires an active HttpContext and HttpRequest but it has no dependency on an ActionContext so it's more widely usable than Url.Content() plus it provides some of the previously discussed additional resolve features.

/// <summary>
/// Resolves of a virtual Url to a fully qualified Url.
///
/// * ~/ ~ as base path
/// * / as base path
/// * https:// http:// return as is
/// * Any relative path: resolved based on current Request path
///   In this scenario ./ or ../ are left as is but prefixed by Request path.
/// * Empty or null: returned as is
/// </summary>
/// <remarks>Requires that you have access to an active Request</remarks>
/// <param name="context">The HttpContext to work with (extension property)</param>
/// <param name="url">Url to resolve</param>
/// <param name="basepath">
/// Optionally provide the base path to normalize for.
/// Format: `/` or `/docs/`
/// </param>    
/// <param name="returnAbsoluteUrl">If true returns an absolute Url( ie. `http://` or `https://`)</param>
/// <param name="ignoreRelativePaths">
/// If true doesn't resolve any relative paths by not prepending the base path.
/// If false are returned as is.
/// </param>
/// <param name="ignoreRootPaths">
/// If true doesn't resolve any root (ie. `/` based paths) by not prepending the base path.
/// If false are returned as is
/// </param>
/// <returns>Updated path</returns>
public static string ResolveUrl(this HttpContext context,
                                string url,
                                string basepath = null,
                                bool returnAbsoluteUrl = false,
                                bool ignoreRelativePaths = false,
                                bool ignoreRootPaths = false)
{
    if (string.IsNullOrEmpty(url) ||
        url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
        url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
        return url;

    // final base path format will be: / or /docs/
    if (basepath == null)
        basepath = context.Request.PathBase.Value ?? string.Empty;

    if (string.IsNullOrEmpty(basepath) || basepath == "/")
        basepath = "/";
    else
        basepath = "/" + basepath.Trim('/') + "/"; // normalize

    if (returnAbsoluteUrl)
    {
        basepath = $"{context.Request.Scheme}://{context.Request.Host}/{basepath.TrimStart('/')}";
    }

    if (url.StartsWith("~/"))
        url = basepath + url.Substring(2);
    else if (url.StartsWith("~"))
        url = basepath + url.Substring(1);
    // translate root paths
    else if (url.StartsWith("/"))
   {
        if(!ignoreRootPaths && !url.StartsWith(basepath, StringComparison.OrdinalIgnoreCase))
        {
            url = basepath + url.Substring(1);
        }
        // else pass through as is
    }
    // translate relative paths
    else if (!ignoreRelativePaths)
    {
        url = basepath + context.Request.Path.Value?.Trim('/') + "/" + url.TrimStart('/');
    }

    // any relative Urls we can't do anything with
    // so return them as is and hope for the best

    return url;
}

Here are a few examples using this version of ResolveUrl()

// based on a root path of /docs/

string path = Context.ResolveUrl("~/fundraiser/s4dd2t2a43/images/images-1.png");
//  /docs/fundraiser/s4dd2t2a43/images/images-1.png

path = Context.ResolveUrl("/fundraiser/s4dd2t2a43/images/images-1.png");
//  /docs/fundraiser/s4dd2t2a43/images/images-1.png

path = Context.ResolveUrl("../fundraiser/s4dd2t2a43/images/images-1.png");
//  /docs/fundraisers/../fundraiser/s4dd2t2a43/images/images-1.png 

path = Context.ResolveUrl("fundraiser/23123", basepath: "/docs2/")
// /docs2/fundraisers/fundraiser/23123

This code will translate:

  • Urls that start with ~/ and ~

  • Urls that start with / and that don't already have the root base path
    (this may cause an occasional mismatch if you really mean to reference something in a parent site. You can disable this behavior optionally)

  • Relative Urls - based on current path (relative Urls are prefixed with base path and current path. In some cases those paths will include ../ or ./ but those will be prefixed by the solved current path. Can be disabled optionally.)

Fully qualified Http Urls are passed through as is. There are options to not process relative and site rooted Urls, which unlike Url.Content() are fixed up in the routine by default. e Url.Content() does.

You can also return an absolute https:// or http:// Url, which is useful in some scenarios especially for external linking.

A non-ASP.NET based ResolveUrl() helper

I also created another helper that doesn't have any dependencies on HttpContext or HttpRequest or anything in ASP.NET for that matter. This requires that you provide some of the parameters explicitly, so this is not quite as convenient as the HttpContext extension method.

Here's that version:

/// <summary>
/// Resolves of a virtual Url to a fully qualified Url. This version
/// requires that you provide a basePath, and if returning an absolute
/// Url a host name.
///
/// * ~/ ~ as base path
/// * / as base path
/// * https:// http:// return as is
/// * Any relative path: returned as is
/// * Empty or null: returned as is
/// </summary>
/// <remarks>Requires that you have access to an active Request</remarks>
/// <param name="context">The HttpContext to work with (extension property)</param>
/// <param name="url">Url to resolve</param>
/// <param name="basepath">
/// Optionally provide the base path to normalize for.
/// Format: `/` or `/docs/`
/// </param>
/// <params name="currentpath">
/// If you want to resolve relative paths you need to provide
/// the current request path (should be a path not a page!)
/// </params>
/// <param name="returnAbsoluteUrl">If true returns an absolute Url( ie. `http://` or `https://`)</param>
/// <param name="ignoreRelativePaths">
/// If true doesn't resolve any relative paths by not prepending the base path.
/// If false are returned as is.
/// </param>
/// <param name="ignoreRootPaths">
/// If true doesn't resolve any root (ie. `/` based paths) by not prepending the base path.
/// If false are returned as is
/// </param>
/// <returns>Updated path</returns>
public static string ResolveUrl(
    string url,
    string basepath = "/",
    string currentPathForRelativeLinks = null,
    bool returnAbsoluteUrl = false,           
    bool ignoreRootPaths = false,
    string absoluteHostName = null,
    string absoluteScheme = "https://")
{
    if (string.IsNullOrEmpty(url) ||
        url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
        url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
        return url;

    // final base path format will be: / or /docs/
    if (string.IsNullOrEmpty(basepath))
        basepath = "/";


    if (string.IsNullOrEmpty(basepath) || basepath == "/")
        basepath = "/";
    else
        basepath = "/" + basepath.Trim('/') + "/"; // normalize

    if (returnAbsoluteUrl)
    {
        if (string.IsNullOrEmpty(absoluteHostName))
            throw new ArgumentException("Host name is required if you return absolute Urls");

        basepath = $"{absoluteScheme}://{absoluteHostName}/{basepath.TrimStart('/')}";
    }

    if (url.StartsWith("~/"))
        url = basepath + url.Substring(2);
    else if (url.StartsWith("~"))
        url = basepath + url.Substring(1);
    // translate root paths
    else if (url.StartsWith("/"))
    {
        if (!ignoreRootPaths && !url.StartsWith(basepath, StringComparison.OrdinalIgnoreCase))
        {
            url = basepath + url.Substring(1);
        }
        // else pass through as is
    }
    else if (!string.IsNullOrEmpty(currentPathForRelativeLinks))
    {
        url = basepath + currentPathForRelativeLinks.Trim('/') + "/" + url.TrimStart('/');
    }

    // any relative Urls we can't do anything with
    // so return them as is and hope for the best

    return url;
}

This works in a similar fashion to HttpContext examples, but depending on what you want to support you you may need to pass in more information about the host and current request:

// based on a root path of /docs/

string path = WebUtils.ResolveUrl("~/fundraiser/s4dd2t2a43/images/images-1.png", "/docs/");
//  /docs/fundraiser/s4dd2t2a43/images/images-1.png

path = Context.ResolveUrl("/fundraiser/s4dd2t2a43/images/images-1.png", "/docs/");
//  /docs/fundraiser/s4dd2t2a43/images/images-1.png

path = Context.ResolveUrl("../fundraiser/s4dd2t2a43/images/images-1.png","/docs/"
                          currentPathForRelativeLinks: "fundraisers/");
//  /docs/fundraisers/../fundraiser/s4dd2t2a43/images/images-1.png 

path = Context.ResolveUrl("fundraiser/23123", basepath: "/docs2/",
                          currentPathForRelativeLinks: "fundraisers/");
// /docs2/fundraisers/fundraiser/23123

path = Context.ResolveUrl("/fundraiser/s4dd2t2a43/images/images-1.png", "/docs/", 
                          absoluteHostName: "localhost:5200");
//  https://localhost:5200/docs/fundraiser/s4dd2t2a43/images/images-1.png

Note that more than likely you'd want to pass in all the parameters for base path, current page and host information, if you're processing the Url generically. This information could come from configuration or some other mechanism if you're outside of the ASP.NET context.

I use a slight variation of this function in my own template engine. In one application that creates Html output from templates it's used to create and fix up links to other topics in this documentation system. Since this code is generated rather than served at runtime, there's no HttpContext or Request so this string based approach is used to resolve Urls correctly for the site that the app is eventually published to.

Summary

Url resolution is something I seem to have reinvented a million times for various different frameworks, going all the way back to my old FoxPro WebFramework nearly 30 years ago. It's a good thing to have at hand, even though it's not something that you need commonly. Since most Web frameworks have some version of Url resolution built in, often they are very closely tied to that framework - if you need to use url resolution outside of that scope you need to do the resolution yourself.

In this post I've described a couple of helpers that do exactly that and even if you don't use these as is, they should give you the basis for resolving Urls in any Web scenario...

Resources

Posted in ASP.NET  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2025