Resolving Paths to Server-Relative Paths in .NET Code
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:
- A
HttpContext
extension method. - 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
- Map Physical Paths with an HttpContext MapPath() Extension Method in ASP.NET Core
- Westwind.AspNetCore Library on GitHub
- HttpContext ResolveUrl() Extension Method Source
- WebUtils ResolveUrl() Method Source
Related posts:
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Keeping Content Out of the Publish Folder for WebDeploy
- Back to Basics: UTC and TimeZones in .NET Web Apps
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