Load .aspx files from class libraries
Published: 2/14/2013 11:43:21 PM
During my time as developer I have created a few ASP.NET web applications. One common task that I often do is to add forms authentication functionality to these web applications. This means a lot of copy/paste from previous applications, and when I recently wanted to change the appearance of the logon screen I realized that it would have been convenient if the logon screen was in a shared class library that I could reference from my applications.
This post will describe how to add .aspx files to a class library and how to reference them from web application projects. The example I will use is based on the previously described scenario: using a common logon screen.
Setting up the solution structure
For this example we need two projects: a class library and a web application project.
- Create a new "Class library" project named LoginSharedLibrary.
- Add a reference to the System.Web assembly.
- Optionally, delete the Class1.cs that is automatically created.
- Create a new "ASP.NET Empty Web Application" project named LoginWebApplication.
- Add a reference to the LoginSharedLibrary project.
- Unfortunately you can't add new web forms to class libraries through the "Add New Item" dialogue, so temporarily add a new web form named Login.aspx to the web application project.
- Open Login.aspx in designer mode and add a Login control to the page.
- Hold the Shift key and drag the Login.aspx web form to the LoginSharedLibrary project. This will move the file to the class library.
- Select Login.aspx and change the build action to "Embedded Resource".
The solution should now look like this:
Creating the virtual path
Normally the .aspx resources are located within the web application project. These files are "physical" in the sense that they exist on the file system and can be access by typing their path in the browser. Files that are located in the class library does not exist on the file system and can therefore not be accessed directly -- you will have to create a virtual path provider in order to access these files.
Creating a virtual path provider is fairly simple. What you need to do is to create a new class in the class library and inherit from the System.Web.Hosting.VirtualPathProvider class, then override the FileExists() and GetFile() methods. I will also override the Initialize method in order to get a list of all the embedded aspx files so that I can determine which virtual files I can use. This is optional and not required, only the FileExists() and GetFile() methods must be overridden.
The FileExists() method is used to determine whether a specific file exists or not. If this method returns true, the GetFile method is called to retrieve the actual file. These methods takes a virtual path as argument, meaning that you would have to examine the path to determine the requested file. I want that it looks as if the virtual files were located in a subfolder ("common"), which is why I verify that it is a valid virtual path at first. If the path is a valid virtual path, and if it references an existing .aspx embedded resource within the class library, it will be returned to the client as a CustomVirtualFile (which is a class that inherits from System.Web.Hosting.VirtualFile).
So, lets look at CustomVirtualFile. The important thing in this class is the overridden Open() method that returns a stream of the embedded resource. I have also added a helper method GetFileNameFromPath() that will extract the filename from the virtual path using a regular expression.
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web.Hosting;
namespace LoginSharedLibrary
{
public class CustomVirtualFile : VirtualFile
{
// Regular expression to extract the filename from the path
private static Regex _fileNameRegex = new Regex(@".*/(?<filename>[\w]*\.aspx)$");
// Filename of the current virtual file
private string _fileName;
public CustomVirtualFile(string virtualPath) : base(virtualPath)
{
_fileName = GetFileNameFromPath(virtualPath);
}
public override string Name
{
get
{
return _fileName;
}
}
public static string GetFileNameFromPath(string virtualPath)
{
Match match = _fileNameRegex.Match(virtualPath);
if(match.Success)
return match.Groups["filename"].Value;
return "Default.aspx";
}
public override Stream Open()
{
// Get the embedded resource
return Assembly.GetExecutingAssembly().
GetManifestResourceStream("LoginSharedLibrary." + _fileName);
}
}
}
The CustomVirtualPathProvider class contains a little bit more code, but not much. The Initialize() method will enumerate all embedded .aspx resources, and FileExists() will use that set to determine whether a requested file can be served. If the file exists, GetFile() will return the corresponding CustomVirtualFile.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
namespace LoginSharedLibrary
{
public class CustomVirtualPathProvider : VirtualPathProvider
{
// VirtualDir is the name of the virtual directory that the aspx files from
// the class library will be located in.
private const string VirtualDir = "common";
// Set of available aspx files
private HashSet<string> _virtualFiles = new HashSet<string>();
// Regular expression for finding embedded aspx resources
private static Regex _validResourceNames =
new Regex(@"^LoginSharedLibrary\.(?<filename>[\w]*\.aspx)$");
protected override void Initialize()
{
// Enumerate all embedded resources and save aspx files.
var resources = Assembly.GetExecutingAssembly().GetManifestResourceNames();
foreach(var resource in resources)
{
Match match = _validResourceNames.Match(resource);
if(match.Success)
_virtualFiles.Add(match.Groups["filename"].Value);
}
base.Initialize();
}
public override bool FileExists(string virtualPath)
{
// If virtual path, check whether matching resource exist.
if(IsPathVirtual(virtualPath))
return _virtualFiles.Contains(
CustomVirtualFile.GetFileNameFromPath(virtualPath));
return base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
// If virtual path, return embedded resource.
if(IsPathVirtual(virtualPath))
return new CustomVirtualFile(virtualPath);
return base.GetFile(virtualPath);
}
// Determines whether path is virtual or not.
private bool IsPathVirtual(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/" + VirtualDir + "/",
StringComparison.InvariantCultureIgnoreCase);
}
}
}
Register the virtual path provider
Once the virtual path provider is created it must be registered in the web application before the virtual path can be used. This is done by calling System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(), for example in global.asax. So, create Global.asax in the web application and add the following code to the Application_Start method:
using LoginSharedLibrary;
using System;
using System.Web.Hosting;
namespace LoginWebApplication
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider());
}
}
}
Testing the code
With the virtual path provider in place and the aspx resource embedded in the class library, we should be all set, right? Well, let's try it out. Start the web application and navigate to ~/common/Login.aspx. If you are unlucky like me, you may get a similar error message:
System.Web.HttpException: Directory 'c:\Demo\LoginWebApplication\common' does not exist. Failed to start monitoring file changes.
It seems like ASP.NET are using some kind of file system watcher to monitor file changes. Since the "common" directory is a virtual path it cannot be found on the file system. To fix this error, override the GetCacheDependency() method in the CustomVirtualPathProvider class and add the following code:
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath,
IEnumerable virtualPathDependencies, DateTime utcStart)
{
if(IsPathVirtual(virtualPath))
return null;
else
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
Now the virtual path should work, and if you navigate to ~/common/Login.aspx again you should now see the login control that has been loaded from the class library:
Disclaimer
The code shown in this post is only meant to demonstrate the concept of loading .aspx files from class libraries. It is not necessarily secure, efficient or pretty.