In this post, Rick Strahl describes strategies and provides helper methods for resolving URLs to server-relative paths in .NET code, extending beyond default ASP.NET Core mechanisms.

Resolving Paths to Server-Relative Paths in .NET Code

By Rick Strahl


ASP.NET Core provides convenient mechanisms to resolve URLs within Controllers and Razor Pages via embedded ~/ links and by using Url.Content(). These, however, are tied specifically to controller or page contexts. So what options exist for resolving URLs elsewhere—such as within middleware or business/business logic layers?

In this post, Rick Strahl explains how to build flexible helpers for URL resolution, making it possible and convenient to generate site-relative paths wherever needed, including scenarios where the web application root may shift (for example, serving from a virtual folder like /docs/).

Why Resolve URLs?

Most traditional ASP.NET Core applications run at the root path (/). But if the site’s root is ever moved to a subfolder, hardcoded /-rooted URLs may break. For dynamic or generated link scenarios—such as in documentation or business logic tying together many resources—there is a need to programmatically resolve URLs, factoring in possible changes to the site’s application base path.

If you rely solely on Url.Content() or Razor ~/ URLs, the solution works well only within View or Controller code. Outside of those (e.g., middleware, business logic, custom rendering engines), you have no direct access to those helpers.

Reviewing ASP.NET Core URL-Resolution

  • In Views:

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

    This auto-resolves to /lib/scripts/helpers.js for root, or /docs/lib/scripts/helpers.js for a site running from /docs/.

  • In Razor/Controller code:

    Url.Content("~/lib/scripts/helpers.js")
    

    Resolves similarly, but requires an ActionContext.

However, these mechanisms are unavailable or difficult to use outside Controllers/Views.

Custom URL Resolution Logic

To make resolution more flexible and context-neutral, Rick proposes two helper methods:

  1. A HttpContext extension method.
  2. A general string-based method.

Helper 1: HttpContext.ResolveUrl() Extension Method

This extension method relies on an active HttpContext and HttpRequest but not on a controller context. It resolves URLs for scenarios starting with:

  • ~/ or ~ (application root)
  • / (site root)
  • Relative paths (resolved using the current request path and base)
  • Returns unmodified URLs for http(s):// links and empty/null values
  • Can optionally return absolute URLs
/// <summary>
/// Resolves a virtual Url to a fully qualified Url.
/// ...
/// </summary>
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;

    if (basepath == null) basepath = context.Request.PathBase.Value ?? string.Empty;
    if (string.IsNullOrEmpty(basepath) || basepath == "/") basepath = "/"; else basepath = "/" + basepath.Trim('/') + "/";

    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);
    else if (url.StartsWith("/")) {
        if(!ignoreRootPaths && !url.StartsWith(basepath, StringComparison.OrdinalIgnoreCase)) {
            url = basepath + url.Substring(1);
        }
    }
    else if (!ignoreRelativePaths) {
        url = basepath + context.Request.Path.Value?.Trim('/') + "/" + url.TrimStart('/');
    }
    // otherwise, return as is
    return url;
}

Example Usage

Suppose the root path is /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

Helper 2: String-Based ResolveUrl()

For usage outside ASP.NET (no HttpContext), Strahl introduces a static method. This requires explicit parameters for base path, and optionally, host/scheme if returning an absolute URL.

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;

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

    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);
    else if (url.StartsWith("/")) {
        if (!ignoreRootPaths && !url.StartsWith(basepath, StringComparison.OrdinalIgnoreCase)) {
            url = basepath + url.Substring(1);
        }
    } else if (!string.IsNullOrEmpty(currentPathForRelativeLinks)) {
        url = basepath + currentPathForRelativeLinks.Trim('/') + "/" + url.TrimStart('/');
    }
    return url;
}

Example Usage

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

This generic method is useful for template engines or documentation generators that process URLs at build time and need flexibility and independence from the ASP.NET request pipeline.

Summary

URL resolution in .NET traditionally ties closely to the runtime environment and framework context. However, there are practical scenarios (like documentation or dynamic content) where resolving paths outside of those contexts is necessary. The helpers described here provide robust mechanisms for resolving URLs—either via an active request or completely framework-agnostic.

These helpers:

  • Work without a controller context (widening applicability)
  • Support virtual/root/relative/absolute URLs
  • Can produce absolute URLs when required

Even if you don’t use these examples directly, they provide a concrete basis for solving similar problems in any .NET web scenario.


Further Reading & Resources


Related posts:


If you found this content useful, consider making a small donation to show your support.


This post appeared first on “Rick Strahl’s Blog”. Read the entire article here