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 http://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.


Comments (30)

  1. Erling Paulsen says:

    Hi David,

    great post. I have a question; Can the VirtualPathProvider handle any type of file, or just the ones mapped in IIS to the Asp.Net runtime?

  2. That’s very interesting topic for me,particularly database driven part as you understand;-)

    But I would like to know more about parser hooks in general-how INamingContainer control can get pre parsed content of itself for example?I mean if we have <c runat="server">….<%if(){%>…<%}%>…</c> I want to get raw text inside <c/>,before(or after) it will be processed by parser-control graph is not very useful in some cases.

    Great post,keep it coming!

  3. Lynn says:

    David, I really enjoy this feature a lot. I’m using the VirtualPathProvider with the file system, but in a slightly dffferent way. What I’m doing is creating structured content in Xml files and using the VirtualPathProvider for on-the-fly page declarative page composition. It works really we so far from a technical perspective, but I haven’t had much chance to measure actual preformance. The best side effect is that the VPP allows for meaningful url names for dynamic files instead of using querystring variables – though this feature can be abused – and that inturn allows much better use of url authorization, and so on. There are a lot of really good wins here.

    A couple of things to note:

    1) Using a VirtualPathProvider and a default document doesn’t work well in IIS 6 as it would require a default.aspx stub. Will this be resolved in IIS 7?

    2) Calling Precompile.axd does not perform compilation on providers added to the site. Is this by design? (Seems like the right idea.)

    3) Will IIS 7 have support for WinFs stores?

  4. Marc Fairorth says:

    David, thanks for an excellent post, very valuable. Would a VPP be appropriate for someone considering an image management system — i.e. store the images (.tiff) in a db instead on on a file store? Can you suggest some resources for learning more? Thanks!

  5. SomeNewKid says:

    Thank you, David, for looking into the problem that I described on the ASP.NET forums.

    The VirtualPathProvider does indeed allow me to override the way in which tilde-based paths are resolved in page directives, such as this:

    <%@ Register TagPrefix="Test" TagName="ChildControl" Src="~/child.ascx" %>

    The VirtualPathProvider does not, however, affect the way in which tilde-based paths are resolved for dynamically loaded user controls, such as this:

    MyPlaceHolder.Controls.Add(LoadControl("~/child.ascx"));

    To have the LoadControl method resolve tilde-based paths in the same way as Register directives requires that the page code-behind forcibly override this method:

    public new Control LoadControl(string relativePath)

    {

    string newPath = relativePath;

    string site = this.Request.QueryString["site"];

    if (String.IsNullOrEmpty(site) == false)

    {

    newPath = VirtualPathUtility.ToAppRelative(newPath);

    newPath = relativePath.Substring(1);

    newPath = "~/PeeudoSites/" + site + newPath;

    }

    return base.LoadControl(newPath);

    }

    With the combination of a VirtualPathProvider and an updated base page class, an ASP.NET application can allow pseudo-sites (child sites) to work as though they were full sites.

    Great stuff! Thanks, David!

  6. davidebb says:

    Erling, the VPP only handles files that are handled by ASP.NET, since it is a part of the ASP.NET runtime. Note that you can star map all requests for a given application to go to ASP.NET: in the IIS configuration, add an entry in the Wildcard application maps pointing to aspnet_isapi.dll (full path).

  7. davidebb says:

    Hi Lynn,

    I’m not much of an expert on IIS7, so I’ll let others answer #1 and #3. You might be able to get around #1 by mapping all request to ASP.NET (see my previous comment), and then use RewritePath to map the directory request to the default document yourself.

    #2: note that precompile.axd no longer exists in the released product (we removed it because of security concerns). You can still do the equivalent from the command line using aspnet_compiler.exe. But to answer your question, precompilation does not work with VirtualPathProviders. I think it could have been made to work in theory, but there were some non-trivial issues, and scheduling made us decide not to support it

  8. davidebb says:

    Hi Mark,

    Yes, I think a VPP could work for your image store. Mostly, it will work well if you want your URL to look like they use a ‘regular’ directory structure instead of being based on quesry string params.

    There may not be much in term of resources right now, though I’m hoping to write a more complete VPP sample at some point.

  9. davidebb says:

    Hi Alister,

    Glad that this solution is working for you!

    You bring up a very good point about it not working for the LoadControl() case. Frankly, I think that this case should have been made to go through VPP.CombineVirtualPaths as well, but for whatever reason, we didn’t do this (you could say it’s a bug).

    Your workaround will work, as long as LoadControl is called directly on your derived class (since LoadControl is not virtual). Also, you may need a similar override in a UserControl base class (since LoadControl lives on TemplateControl). Kind of a pain, but it least it gets you going!

  10. lynn says:

    David, thanks for the heads up on the precompile.axd removal.I haven’t gotten to the RTM yet.

  11. Alex says:

    The idea of using it to retrieve pages from a database is very interesting and might work for a project that I’m discussing right now.

    If we were to retrieve the pages directly from the db, one question I would have is about compilation and performance.

    1) Will the page be compiled the first time it is retrieved from the DB or would it be recompiled every time?

    2) When the page is compiling will other users of other pages also have to wait while the page compiles or will they be able to continue working with their pages while the newly retrieved pages compile.

    3) Could the code-behind also be retrieved from the DB or just the ASPX page?

  12. davidebb says:

    Alex,

    1. Compiled pages are cached, and not recompiled unless your VPP indicates they are out of date.

    2. Generally, ASP.NET only allows one compilation to happen at a time. However, other requests that don’t require compilation won’t be blocked.

    3. Yes, the VPP applies to both the page and its code file.

  13. Jun Meng says:

    Quick question:

    Could you just set the virtual directory as "Application" in IIS? This way, you do not need any coding for "~/".

  14. Sharad Kumar says:

    David,

    Thanks loads for the post. I’m looking forward to utilize CombinePath approach for my following case:

    1. Multiple Virtual WebSites, with different domains are pointing to same application. Through a HttpModule, I map to corresponding folders of websites based on their host. This allows me to simplify:

    a. http://www.domain1.com/default.aspx >> to >> http://www.domain1.com/app_websites/domain1/default.aspx

    b. http://www.domain2.com/default.aspx >> to >> http://www.domain2.com/app_websites/domain2/default.aspx

    Above works absolutely fine with Context.RewritePath, functionally. Problem I’m facing is to base my Urls correctly to Theme resources, with respect to default.aspx which is in root (as in Url). Where does basing, w.r.t. Theme resources happen? How can make ../../App_Themes/Red/Style.css to /App_Themes/Red/Style.css w.r.t. /Default.aspx (virtual), which is actually inside domain folder?

    Any help shall be greatly useful. Thanks.

    — Sharad

  15. davidebb says:

    Jun,

    Yes, you could certainly do this, but the premise here is that we want to be "running multiple sites under a single actual ASP.NET application". Why? Because it is much lighter weight than having multiple applications, so it can potentially scale to a very large number of apps.

    David

  16. davidebb says:

    Sharad,

    Maybe one approach that would work is to star map all requests to ASP.NET in IIS, in order to have them all go through your HttpModule. From there you could then fix up the paths?

    David

  17. Scott says:

    David,

    This is exactly what I needed. Thanks, and excellent work!

  18. So, in ASP.NET 2.0 we have this niftty feature that noone really understands called the Virtual Path…

  19. Ed says:

    Hi,

    This is a great feature for content management. I’ve got it working with my cms database to load content, but how do I exclude the directory containing the cms admin system itself?

    http://www.mysite.com/mydir/page.aspx

    Something like this loads fine from the db.

    http://www.mysite.com/cms/default.aspx

    This page doesn’t exist in the database so is a 404. I need to somehow exclude /cms/ and all its children from the VPP so they can be served normally.

    Any pointers would be greatly appreciated!

    Cheers,

    Ed

  20. davidebb says:

    Hi Ed,

    You should be able to exclude content by simply forwarding the calls to don’t want to handle to ‘Previous’.  e.g. if you get a call to GetDirectory for your cms dir, just return Previous.GetDirectory(virtualDir).

    David

  21. Ed says:

    Hi David,

    That doesn’t seem to be working.

    I haven’t changed the GetDirectory method from Scott Guthrie’s original example which returns Previous.GetDirectory(virtualDir) if the db access layer’s file record data is null.

    Any thoughts on how I could debug this would be great.

    Cheers,

    Ed

  22. Ed says:

    Hi, found the problem. In the code sample I downloaded the FileExists and DirectoryExists methods were returning false when the file record data was null. These need to be changed to:

    return Previous.FileExists(virtualPath);

    and

    return Previous.DirectoryExists(virtualDir);

    Cheers,

    Ed

  23. black says:

    that’s just what i want! great!

    Sprite Builder – combine separate images into one sprite.

    http://www.yaodownload.com/video-design/animationdesigntools/sprite-builder_animationdesigntools.htm

  24. bsmith says:

    I have been looking for an article showing how to implement the VirtualPathProvider to pull Micorsoft Office Documents from a database.  From what I have found so far, it is a little more involved.  Any advice you could offer on this topic would be great.

  25. Jesse says:

    I’m trying to do something different, but this seems like it might be the solution. Could I use VPP to let me share an "images" directory between two web apps? I.e.,

    wwwroot/

    app1/

    app2/

    images/

    Where the "images/" part of the path gets dynamically rewritten to an absolute filesystem path (wwwroot/images) instead of an app-rooted path (appN/images)?

    Or is there an easier way to do this?

  26. davidebb says:

    Jesse, maybe an easier way to do this is to use NTFS junctions to make a single directory appear in multiple places.  e.g. start by looking at this tools: http://www.sysinternals.com/Utilities/Junction.html

  27. Jesse says:

    Thanks, David! That’s *exactly* what I needed. Except that my problem’s on a shared server, where I can’t shell out to exe’s. But I found a C# wrapper for the DFS API’s: http://www.pinvoke.net/default.aspx/netapi32.NetDfsAdd

    Hopefully, this will do the trick.

  28. ASP.Net 2.0&amp;nbsp;is bundled with some great technology, especially what is available through the&amp;nbsp;provider…

  29. Fabrice JEAN-FRANCOIS says:

    Yo Guys,

    I have found a way to register my VirtualPathProvider with the precompile option…

    It’s really easy with only 9 lines of code.

    Why do I need this functionnality? Becoz I’m working in a bank that need to share its masterpage and that compels us to precompile and create a single assembly (aspnet_merge) with versioning for all our websites…

    So, what is my secret ?  Very easy. The answer is DynamicMethod. I call a Microsoft internal method to register my VirtualPathProvider.

    Nevertheless, there are limitations. For exemple, i suppose it does not work for all situation (since Microsoft doesn’t want us to precompile). Moreover, I am now dependent of the CLR version and implementation (Microsoft can still change its code without my permission 😀 )

    May the code be with you…

  30. Recently one of the engineers on my team (Amitkumar Sharma) got this issue reported by the customer where