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.
Comments
- Anonymous
November 27, 2005
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? - Anonymous
November 27, 2005
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! - Anonymous
November 28, 2005
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? - Anonymous
November 28, 2005
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! - Anonymous
November 28, 2005
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! - Anonymous
November 29, 2005
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). - Anonymous
November 29, 2005
The comment has been removed - Anonymous
November 29, 2005
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. - Anonymous
November 29, 2005
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! - Anonymous
November 29, 2005
David, thanks for the heads up on the precompile.axd removal.I haven't gotten to the RTM yet. - Anonymous
December 13, 2005
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? - Anonymous
December 21, 2005
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. - Anonymous
January 12, 2006
Quick question:
Could you just set the virtual directory as "Application" in IIS? This way, you do not need any coding for "~/". - Anonymous
January 21, 2006
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. www.domain1.com/default.aspx >> to >> www.domain1.com/app_websites/domain1/default.aspx
b. www.domain2.com/default.aspx >> to >> 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 - Anonymous
January 23, 2006
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 - Anonymous
January 23, 2006
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 - Anonymous
January 25, 2006
David,
This is exactly what I needed. Thanks, and excellent work! - Anonymous
March 02, 2006
So, in ASP.NET 2.0 we have this niftty feature that noone really understands called the Virtual Path... - Anonymous
March 16, 2006
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?
www.mysite.com/mydir/page.aspx
Something like this loads fine from the db.
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 - Anonymous
March 16, 2006
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 - Anonymous
March 16, 2006
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 - Anonymous
March 16, 2006
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 - Anonymous
April 27, 2006
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 - Anonymous
April 28, 2006
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. - Anonymous
May 10, 2006
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? - Anonymous
May 10, 2006
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 - Anonymous
May 10, 2006
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. - Anonymous
June 29, 2006
ASP.Net 2.0&nbsp;is bundled with some great technology, especially what is available through the&nbsp;provider... - Anonymous
July 19, 2006
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 :-D )
May the code be with you... - Anonymous
December 09, 2006
Recently one of the engineers on my team (Amitkumar Sharma) got this issue reported by the customer where