Running ASP.NET Core Applications in an IIS Subfolder Application

ASP.NET Core applications by default want to run in a root folder - and to be fair that's the 99% use case. But there are those occasional situations where you want to run a Web site in a sub folder rather than on the root of the Web site.
In this post I review what's required to run ASP.NET with a PathBase - which works with any Web server - and then specifically discuss how to set this up on IIS, which is a little more complicated than it should be.
Why would I need to run out of a SubFolder?
The specific scenario that I ran into was that recently I updated my old custom Blog engine, and decided to pull all my secondary blogs onto my own server from various blog publishing sites. I've always run my blog on my own hosted server, but a couple of product blogs ran on different hosting sites which I never used because they were terrible.
With my recent site blog update - I finally moved the old WebForms app to .NET Core - and the much simplified deployment set up that comes with it, I decided to just run everything in house for these relatively low volume sites and get better, more consistent theming and a much easier publishing pipeline using Markdown Monster (using a new custom protocol - more on that in another post).
In any case the scenario here is that I have a root product site:
and I now want to run the blog site out of a /blog/ sub-folder, rather than as a root site. So:
rather than a separate new root Web site like:
(which is what I started with then aborted)
For SEO it's often beneficial to run everything on the same site, but it's also cleaner and more consistent with other sub folders like /docs and /support. Running a root site also requires more setup - yet another certificate and custom bindings to manage and so on - so using a subfolder is effectively more lightweight from a config perspective.
But using a sub-folder site requires more care is given how Urls are created in the application as we'll see.
Creating an Application in a Subfolder
There are two parts to the process of running an application in a subfolder:
- Configuring ASP.NET for running from a subfolder
- Configuring the Web Server for a subfolder
Setting up ASP.NET For running with a Subfolder
Turns out setting up ASP.NET to run from a subfolder is pretty easy to do as there's a dedicated middleware to set up a custom PathBase as ASP.NET likes to call it.
// in program.cs
app.UsePathBase("/blog/");
This code goes into program.cs after the builder has created an app instance, using the provided services. You'll want to do this near the top of the app middleware declarations to ensure the folder is respected all the way through the middleware pipeline - you'll want this before authentication and static files and certainly before any routing middleware. I have it at the very top of the pipeline.
I tend to parameterize the PathBase parameter with a configuration value, because I'm duplicating the same application in multiple folders. So, in my Weblog application it looks like this:
// config from DI initialization or wlApp.Configuration static
if (!string.IsNullOrEmpty(config.VirtualPath) && config.VirtualPath != "/")
{
app.UsePathBase($"/{config.VirtualPath}/");
}
I have 3 blog sites - one of which runs as root and two which run in /blog/ subfolders. By parameterizing I can customize whether they run out of a subfolder or not without recompilation.
So what does .AddPathBase() actually do?
It's used to resolve Urls internally, based on the path you specify. Anytime ASP.NET creates a path dynamically for routes, uses ~/ in Views or Pages, or via Url.Content() or other IUrlHelper use it automatically fixes up the path with the provided path base.
So instead of returning /images/someimage.png which you get for a root site, you get /blog/images/someimage.png for example.
If you use implicit routing or you stick to using ~/ paths in your Views/Pages, ASP.NET does most of the heavy lifting without you having to do anything else. This is nothing new - class ASP.NET used to do that as well, but it's pretty nice that this is handled.
Fixing up Root Paths with ~/ and an ApplicationBasePath
This does mean you need to be more vigilant about how you root your Urls in your application. So rather than <img src="/images/someimage.png" /> you should use <img src="~/images/someimage.png" /> to ensure the appropriate path is used.
You can quickly find and replace all instances of hard coded root paths by doing a Find in Files Search (Ctrl-Shift-F) in your IDE and doing a search and replace for ="/ and replacing with ="~/. This should capture most scenarios in any physical files.
In code you can access the IUrlHelper interface in Razor views or injected into controllers or methods.
In a Razor Page or View
var rootPath = Url.Content("~/images/someimage.png");
You can also inject IUrlHelperFactory (WTF Microsoft?) and then retrieve the IUrlHelper in a somewhat convoluted way that only an ivory tower architect could love:
public class MyService
{
private readonly IUrlHelper _url;
public MyService(
IUrlHelperFactory factory,
IActionContextAccessor actionContextAccessor)
{
_url = factory.GetUrlHelper(
actionContextAccessor.ActionContext);
}
public string Resolve()
{
return _url.Content("~/images/logo.png");
}
}
If you don't want to deal with this and just have a couple of generic methods that work anywhere, the Westwind.AspNetCore package has a couple of generic helpers:
- HttpContext.ResolveUrl() extension method
- WebUtils.ResolveUrl() - you provide a base path as a string and it resolves with that
Running locally with a SubFolder
If you create your app this way and you run it locally using the Kestrel Web server with dotnet run or from your IDE you will see the site come up in a subfolder:

The url includes the /docs/ subfolder:
https://localhost:5001/docs/
If you stuck to using ~/ root paths everything is bound to just work the same as if you were running from the root folder.
Note that you'll want to change your dev launchSettings.json to reflect the new launchUrl:
"Westwind.Weblog.MarkdownMonster": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001",
// THIS!
"launchUrl": "https://localhost:5001/blog/"
},
Configuring IIS for a Subfolder Application
While the UsePathBase() middleware handles the ASP.NET Core side seamlessly, IIS requires some extra work to create an Application under your root website.
Note that IIS requires that each ASP.NET application requires its own dedicated ASP.NET Application Pool. Only a single ASP.NET Application can run inside of any given Application Pool, so unlike classic .NET sites, you can't share a single application pool for multiple Core apps.
IIS has Web Sites and Applications, which are very similar. Web Sites have extra configuration related to Host mappings and bindings, but otherwise Web Sites and Applications behave very similarly.
Here's what a folder based Application looks like in the IIS Manager:

Note that the folder mapping doesn't have to match a physical folder below the Web site - it can point to any location on disk. However, in most cases I put the actual site content into the relative folder for consistency in finding it later 😄.
As mentioned you need to have a dedicated Application Pool for this application - or at least an AppPool that doesn't have another ASP.NET application in it. That AppPool should be configured for No Managed Code.

Make sure you use an identity (user) that matches the rights that you need for your application. My apps tend to have a few configuration related settings that get written out so I have to use an account that has some additional rights.
Once this is set up and you've published your app, IIS will handle the initial request routing and pass the correct path information to the ASP.NET Core module to give you the same behavior that you see on your local install.

Recommendations
If you're like me and you're doing this switch to running a sub-folder at the last minute after running and testing as a Root Site - I'd highly recommend you take a breather 😄 and spend a bit of time testing locally with the subfolder.
I initially built and worked with the site using a Root Web Site and only towards the very end tested with the sub-folder. For the most part the site ran with simple fixes but there were a few things that were missed like some Javascript client code that didn't use the base path. Take the time to test locally with the sub folder before publishing! Some of the errors you might run into are subtle and easy to miss.
To summarize these are some of the things that you have to probably fix or at least check when going from Site to Folder based:
All .cshtml, .razor Pages
All view pages can automatically fix up pages that reference via "~/" paths. Maybe you were diligent and started doing this right way. I rarely ever do this right - I do some with ~/ and some not which is no better than not doing it all 😄.
Luckily fixing up View pages is pretty straight forward: You can do a simple search and replace:
- Select *.cshtml, *.razor etc.
- Search for
="/ - Replace with
="~/

Code Fix ups
You can do something similar for your .NET code, but you have to be a lot more selective and you can't do a search and replace.
- Select *.cs, *.cshtml (cshtml for code blocks that build Urls)
- Search for
"/
You want to search code both in your CS files and any code snippets in your Views/Pages.
Hopefully there shouldn't be a lot of those in your code because generally hard coding Urls is a bad idea. Typically this only happens if Urls need to be built up based on a host of input parameters.
var redirectUrl = "/somedeeplink/in/the/site";
changed to:
var redirectUrl = wlApp.Configuration.ApplicationBasePath + "somedeeplink/in/the/site";
Again, this should be pretty rare but worth checking for. This search is likely to produce a lot of false positives that don't require any changes.
Javascript Code
Javascript code is little more tricky. If you have script code that's making server calls, you may have to fix up paths in your scripts. Scripts aren't executable code and the ASP.NET parser doesn't touch them as the files are served as static resources. It's also common that you provide Urls as strings.
It's a lesson I've learned a long time ago - almost every application that calls to the server requires a configurable value to for a base path to call the server. There are lots of ways to do this and in SPA applications I always have a config object to do this.
However, for traditional server based Web apps that have only a few isolated JavaScript callbacks to the server, there is no common entry point for scripts - they are just loaded randomly.
In this application I do this the brute force way by providing a base path in a global script variable that gets embedded into the _Layout.cshtml page. Since _Layout.cshtml touches every page this is as close as I get to a global client side entry point. You can also put this in a script file but you have to make sure it gets loaded early enough that all other scripts can see it.
The idea is that I create a global variable - or rather a global object with an embedded variable - that is accessible from any script. The script is defined at the top of the page in the header so it's visible to any code that follows.
I use another helper class from Westwind.AspNetCore helper for this:
@
{
// at the top of _Layout.cshtml
// creates an object with props for the value(s) below
var scriptVars = new ScriptVariables("window.page");
scriptVars.Add("basePath", wlApp.Configuration.ApplicationBasePath);
}
<!DOCTYPE HTML>
<html>
<head runat="server">
<title>@(ViewBag.Title ?? wlApp.Configuration.ApplicationName)</title>
<script>
// expands into an object with props
@scriptVars.ToHtmlString();
</script>
ScriptVariables is a helper class that makes it easy to create a Json safe object of values you want to embed into the page from your .NET code. You basically add variables which are then serialized - Javascript and Html safe into the document when you use @scriptVars.ToHtmlString().
In this case it produces an object with a single variable, but typically I end up with a handful of 'global' page level properties that need to be passed through:
window.page = {
basePath: "https://markdownmonster.west-wind.com/blog/"
};
You can decide whether you want to use just /blog/ or the fully qualified path as I'm doing here.
I'm using:
scriptVars.Add("basePath", wlApp.Configuration.ApplicationBasePath);
which is configured value that I store in the app to have quick and easy access to the full site url. In the case of this _Layout.cshtml page you could also use:
scriptVars.Add("basePath", Url.Content("~/");
which produces the site relative /blog/ base path.
Now any scripts on the page - both in the page itself or in any script files can look at page.basePath and use that to fix up any Urls as necessary:
deletePost = ()=> {
if (confirm('Are you sure you want to delete this post?')) {
// THIS
var url = page.basePath + "posts/@post.Id";
ajaxJson(url, null,
(res) => {
// AND THIS
location.href = page.basePath + "posts";
},
(err) => {
alert("Error deleting post: " + err.responseText);
},
{ HttpVerb: "DELETE" });
}
}
While this takes a little bit to set up, I always kick myself for not doing this right from the start. Having a base path for JS calls is just good practice, to ensure that you can move the Urls around as needed because you never know... as in this case.
Summary
Running Web sites out of a virtual folder is not all that common, but for many low impact sites I'm actually finding myself using them more often than I would have thought. When you need to do it, it's good to know that it's possible and not as complicated as I thought it might be.
The most time consuming part of the process isn't getting the site to run, but rather it's making sure that the site's links can run in the subfolder environment and resolve properly. Luckily fixing up links can be done relatively easily with ASP.NET due to its excellent internal support for resolving Urls. A few Search and Replace cycles go a long way to making sure apps can run in a subfolder application environment.
Other Posts you might also like
- Map Physical Paths with an HttpContext.MapPath() Extension Method in ASP.NET
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Getting the Client IP Address in ASP.NET Core
- Adding Default Assemblies, Namespaces and Control Prefixes in Web.Config
- Resolving Paths To Server Relative Paths in .NET Code