Overriding ASP.NET combine behavior using a VirtualPathProvider

This article applies to ASP.NET 2.0.

Background

This article originated from a customer question on the ASP.NET site.  What they are trying to achieve is running multiple sites under a single actual ASP.NET application.  This can be useful to avoid the overhead of having a different appdomain per site.  So the general idea is to have a single application, and to use sub-directories to represent the site.  Let's call them 'pseudo-sites' as they are really just directories from the point of view of ASP.NET.

For example, the app could have this structure:

MyApp
PseudoSites
Site1
page.aspx
uc.ascx
Site2
Site2's files...
 

Such pseudo-sites will necessarily have a number of limitations: e.g. they won't be able to each have their own bin, App_Code, and other top level directories, since these can only exist at the top level of a real ASP.NET application.  In spite of these limitations, the structure can be useful for apps that don't needs to have those directories.

 

The issue we're trying to solve: how to make path resolution work

The main issue that this article deals with is the fact that path resolution will by default not work correctly when using such a structure.  e.g. suppose /MyApp/PseudoSites/Site1/page.aspx has:

<%@ Register Src="~/uc.ascx" TagName="uc" TagPrefix="uc1" %>

Recall that '~' means "the root of the app".  Clearly "~/uc.ascx" means to refer to uc.ascx in the same pseudo-site as page.aspx.  But ASP.NET will not see it that way, as the real root of the app is just "/MyApp".  Instead, this will resolve to "/MyApp/uc.ascx", which is not where the file is.

One obvious solution is to use relative paths instead of app relative paths.  e.g. here you could write src="uc.ascx" mce_src="uc.ascx" and it would work fine.  This is a fine thing to do in some cases, but in many other cases, you are much better off using app relative paths, as you are then free to move files around without having to worry about the relative locations always staying the same.

So the question is: how can we make app relative paths (as well as absolute path, e.g. "/Site1/page.aspx") work correctly in the pseudo-site environment?

 

VirtualPathProvider to the rescue

ASP.NET 2.0 introduces the ability to hook deep into the way it deals with files via something called a VirtualPathProvider.  Implementing a full VirtualPathProvider is somewhat involved, and is usually done to serve files out of an alternate store, like a database.  Doing this is beyond the scope of this article (though I'd like to write more about it if there is interest!), and we will look at only one VirtualPathProvider method: CombineVirtualPaths. This method is called whenever the parser needs to resolve paths, which is exactly what we need to solve our problem!

The code below shows a sample implementation of CombineVirtualPaths.  You will need to adapt it to your situation but it demonstrates the principle.  To try this code, simply put it somewhere in the App_Code directory (of your real app, not pseudo app!).

Note: AppInitialize is a special method that gets called automatically at startup when it is found somewhere is App_Code.  You could alternatively register the VirtualPathProvider from global.asax (in Application_OnStart) or an HttpModule.

 

using System;
using System.Web;
using System.Web.Util;
using System.Web.Hosting;

public class SimpleVPP : VirtualPathProvider {
    public static void AppInitialize() {
HostingEnvironment.RegisterVirtualPathProvider(new SimpleVPP());
    }

    public override string CombineVirtualPaths(string basePath, string relativePath) {

        // If the path is relative, let normal processing happen
        if (!VirtualPathUtility.IsAbsolute(relativePath))
            return base.CombineVirtualPaths(basePath, relativePath);

        // Determine the pseudo site from the request. To demonstrate, we just get it from the
// query string, but it could come from other places, like the http host header
        string site = HttpContext.Current.Request.QueryString["site"];

        // If we couldn't, default to normal processing
        if (site == null)
            return base.CombineVirtualPaths(basePath, relativePath);

        // Make it app relative (i.e. ~/...)
        relativePath = VirtualPathUtility.ToAppRelative(relativePath);

        // Remap the virtual path to be inside the correct pseudo site
        return "~/PseudoSites/" + site + relativePath.Substring(1);
    }
}

That's basically it!  With this code, the situation described above will be able to run, since your code is driving the path resolution.  Basically, you get to give whatever meaning you want to '~'.

A couple more notes about this:

  • The name of the parameter 'relativePath' in CombineVirtualPaths is misleading, since this is actually called for all paths, not just relative. And of course, if that were not the case, our solution wouldn't work!
  • To test the example above, you would need to request something like https://localhost/MyApp/PseudoSites/Site1/Foo.aspx ?site=Site1, because that's what the sample CombineVirtualPaths expects. In a real world app, you would probably not use that.

Note on using a VirtualPathProvider with a precompiled site

As some of you found, if your site if precompiled, your VPP is not used.  I wish we had made this scenario work, but I guess it fell through due to scheduling.  We basically ended up artificially disabling the scenario because we didn't have time to test it properly.  Someone posted a workaround using private reflection.  It is definitely a hack (and may break in later versions, though that's not likely), and I can't guarantee that it works well in all scenario, but if it can unblock you, give it a try.