Websites tend to be organic—they grow and change over time. This can create problems when other applications link to your application. You need some way of modifying your website without breaking all the existing links to it.
In this chapter, you learn how to remap URLs. In other words, you learn how to serve a different page than the page a user requests. In the first section of the chapter, you learn how to remap URLs in the web configuration file.
Next, you learn how to remap URLs by creating a custom HTTP module. Using a module is useful when you need to support wildcard matches and other types of pattern matching when remapping a URL.
Finally, you learn how to use the VirtualPathProvider
class to remap URLs. You learn how you can store all your website pages in a database. In the last section of this chapter, a simple Content Management System (CMS) is built with the VirtualPathProvider
class.
The simplest way to remap a URL is to specify the remapping in your application’s web configuration file. For example, the web configuration file in Listing 21.1 remaps the Home.aspx
page to the Default.aspx
page.
Example 21.1. Web.Config
<configuration> <system.web> <urlMappings> <add url="~/Home.aspx" mappedUrl="~/Default.aspx"/> </urlMappings> </system.web> </configuration>
The configuration file in Listing 21.1 contains a <urlMappings>
element. This element can contain one or more elements that remap a page from a URL to a mapped Url.
The code samples in this section can be found in the UrlMappingsApp
application on the CD that accompanies this book.
The mappedUrl
attribute can contain query strings. However, it cannot contain wildcards. You can use the <urlMappings>
element only when performing simple page-to-page mappings.
After you add the web configuration file in Listing 21.1 to your application, any requests for the Home.aspx
page are modified automatically to requests for the Default.aspx
page. It doesn’t matter whether the Home.aspx
page actually exists. If the Home.aspx
page does exist, you can never open the page.
The tilde character (~) has a special meaning when used with a path. It represents the current application root. A forward slash (/) at the start of a URL, on the other hand, represents the website root.
You can use the tilde only with properties of ASP.NET controls. For example, you can use it with the ASP.NET Image control’s ImageUrl
property, but you cannot use it with the HTML <img> src
attribute.
In code, you can use the tilde character with a path by using the Page.ResolveUrl()
method. This method automatically expands the tilde to the application root.
When working with remapped URLs, you often need to determine the original URL that a user requested. For example, you might want to display a message that tells users to update their bookmarks (favorites) to point to the new URL.
You can use the following to determine the current URL:
The last property automatically replaces the name of the web application with a tilde (~) character.
For example, the Default.aspx
page in Listing 21.2 illustrates all three properties.
Example 21.2. Default.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> void Page_Load() { if (String.Compare(Request.Path, Request.RawUrl, true) != 0) lblMessage.Text = "The URL to this page has changed, " + "please update your bookmarks."; } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <style type="text/css"> html { font:14px Georgia,Serif; } .message { border:Dotted 2px red; background-color:yellow; } </style> <title>Default Page</title> </head> <body> <form id="form1" runat="server"> <div> <h1>The Default Page</h1> <p> <asp:Label id="lblMessage" CssClass="message" Runat="server" /> </p> The original request was for: <blockquote> <%=Request.RawUrl%> </blockquote> which got remapped to: <blockquote> <%= Request.Path %> </blockquote> and the application relative version is: <blockquote> <%= Request.AppRelativeCurrentExecutionFilePath %> </blockquote> </div> </form> </body> </html>
If you request the Home.aspx
page, the request is remapped to the Default.aspx
page by the web configuration file in Listing 21.1. The Page_Load()
event handler displays a message asking users to update their bookmarks when the RawUrl
does not match the path (see Figure 21.1).
Each property displayed in the body of the page displays a different value:
Request.RawUrl = /UrlMappingsApp/Home.aspx Request.Path = /UrlMappingsApp/Default.aspx Request.AppRelativeCurrentExecutionFilePath = ~/Default.aspx
The <urlMappings>
configuration element discussed in the previous section performs a very simple task. It remaps one page to another. However, you’ll quickly discover that you need to perform more complex remappings.
For example, imagine that you have a database that contains a table of product categories and a table of products. You want your website’s users to request a URL that contains a product category and be able to see matching products. For example, if someone requests the /Products/Soda.aspx
page, you want to display all the products in the Soda category. If someone requests the /Products/Milk.aspx
page, you want to display all the products in the Milk category.
In that case, you need to use a wildcard when matching URLs. When someone requests any path that matches the pattern /Products/*
, you want to redirect the user to a page where you can display matching products for the category specified in the path.
In this section, we create a custom HTTP module that remaps one URL to another. The module supports regular expression matching. Therefore it supports wildcard matches.
The code samples in this section are located in the UrlRemapperApp
application on the CD that accompanies this book.
The code for the custom module—named UrlRemapper
—is contained in Listing 21.3.
Example 21.3. UrlRemapper.cs
using System; using System.Web; using System.Xml; using System.Web.Caching; using System.Text.RegularExpressions; namespace AspNetUnleashed { public class UrlRemapper : IHttpModule { public void Init(HttpApplication app) { app.BeginRequest += new EventHandler(app_BeginRequest); } public void app_BeginRequest(Object s, EventArgs e) { // Get HTTP Context HttpApplication app = (HttpApplication)s; HttpContext context = app.Context; // Get current URL string currentUrl = context.Request.AppRelativeCurrentExecutionFilePath; // Get URL Mappings XmlDocument urlMappings = GetUrlMappings(context); // Compare current URL against each URL from mappings file XmlNodeList nodes = urlMappings.SelectNodes("//add"); foreach (XmlNode node in nodes) { string url = node.Attributes["url"].Value; string mappedUrl = node.Attributes["mappedUrl"].Value; if (Regex.Match(currentUrl, url, RegexOptions.IgnoreCase).Success) context.RewritePath(mappedUrl); } } private XmlDocument GetUrlMappings(HttpContext context) { XmlDocument urlMappings = (XmlDocument)context.Cache["UrlMappings"]; if (urlMappings == null) { urlMappings = new XmlDocument(); string path = context.Server.MapPath("~/UrlMappings.config"); urlMappings.Load(path); CacheDependency fileDepend = new CacheDependency(path); context.Cache.Insert("UrlMappings", urlMappings, fileDepend); } return urlMappings; } public void Dispose() { } } }
Notice that the class in Listing 21.3 implements the IHttpModule
interface. An HTTP module is a special class that executes whenever you make a page request. HTTP Modules are discussed in detail in Chapter 27, “Working with the HTTP Runtime.”
The module in Listing 21.3 includes an Init()
method. This method adds an event handler for the Application BeginRequest
event. The BeginRequest
event is the first event that is raised when you request a page.
The BeginRequest
handler gets a list of URL remappings from an XML file named UrlMappings.config
. The contents of this XML file are cached in memory until the UrlMappings.config
file is changed on the hard drive.
Next, the module iterates through each remapping from the XML file and performs a regular expression match against the current URL. If the match is successful, then the Context.RewritePath()
method is used to change the current path to the remapped path.
Before you can use the module in Listing 21.3 in an application, you must first register the module in your application’s web configuration file. The web configuration file in Listing 21.4 contains an <httpModules>
element that includes the UrlRemapper
module.
Example 21.4. Web.Config
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <httpModules> <add name="UrlRemapper" type="AspNetUnleashed.UrlRemapper" /> </httpModules> </system.web> </configuration>
A sample UrlMappings.config
file is contained in Listing 21.5.
Example 21.5. UrlMappings.config
<urlMappings> <add url="~/Home.aspx" mappedUrl="~/Default.aspx" /> <add url="/Products/.*" mappedUrl="~/Products/Default.aspx" /> </urlMappings>
The XML file in Listing 21.5 contains two remappings. First, it remaps any request for the Home.aspx
page to the Default.aspx
page. Second, it remaps any request for any page in the Products directory to the Default.aspx
page located in the Products folder.
The second mapping uses a regular expression to match the incoming URL. The .*
expression matches any sequence of characters.
The Default.aspx
page in the Products folder is contained in Listing 21.6.
Example 21.6. Products/Default.aspx
<%@ Page Language="C#" %> <%@ Import Namespace="System.IO" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> void Page_Load() { if (!Page.IsPostBack) { string category = Path.GetFileNameWithoutExtension(Request.RawUrl); ltlCategory.Text = category; srcProducts.SelectParameters["Category"].DefaultValue = category; } } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .grid td,.grid th { padding:4px; border-bottom:solid 1px black; } </style> <title>Products</title> </head> <body> <form id="form1" runat="server"> <div> <h1> <asp:Literal ID="ltlCategory" runat="server" /> </h1> <asp:GridView id="grdProducts" DataSourceID="srcProducts" CssClass="grid" GridLines="None" AutoGenerateColumns="false" Runat="server"> <Columns> <asp:BoundField HeaderText="Product Name" DataField="Name" /> <asp:BoundField HeaderText="Price" DataField="Price" DataFormatString="{0:c}" /> </Columns> </asp:GridView> <asp:SqlDataSource id="srcProducts" ConnectionString="<%$ ConnectionStrings:Products %>" SelectCommand="SELECT Products.* FROM Products JOIN Categories ON Products.CategoryId=Categories.Id WHERE Categories.Name=@Category" Runat="server"> <SelectParameters> <asp:Parameter Name="Category" /> </SelectParameters> </asp:SqlDataSource> </div> </form> </body> </html>
The Page_Load()
event handler in Listing 21.6 grabs the path of the original request, using the Request.RawUrl
property. Next, it extracts the filename from the path, using the System.IO.Path.GetFileNameWithoutExtension()
method. Finally, it assigns the name of the page (the category name) to a Label
and SqlDataSource
control. Products that match the category are displayed in a GridView
control.
For example, if you request the /Products/Soda.aspx
page, then all the products in the Soda category are displayed (see Figure 21.2). If you request the /Products/Milk.aspx
page, then all products in the Milk category are displayed.
The VirtualPathProvider
class enables you to abstract the pages in a web application from the file system. In other words, it enables you to store your ASP.NET pages any way you please.
For example, you can use the VirtualPathProvider
class to store all the pages in your application in a database. This would be an appropriate choice when you need to build a CMS. If you store pages in a database, then users can update the pages easily in an application through an HTML form interface and save the changes to the database.
The sample application at the end of this book uses the VirtualPathProvider class to store code samples in the database. Users of the website can execute the code samples directly from the database. For more information, see Chapter 34, “Building a Code Sample Website.”
In this section, you learn how to store the pages in an ASP.NET application in a Microsoft SQL Server 2005 Express database. But first, it’s a good idea to examine the classes related to the VirtualPathProvider
class in more detail.
Unfortunately, you can’t use the VirtualPathProvider
with every type of file. In particular, the following types of files must always be located on the file system:
Global.asax
file
Web.Config
files
App_Data folder
App_Code folder
App_GlobalResources folder
App_LocalResource folders
Bin folder
Every other type of file is fair game. This includes ASP.NET pages, User Controls, Themes, and Master Pages.
The VirtualPathProvider
class is a MustInherit
(abstract) class. It contains the following methods, which you can override:
CombineVirtualPaths()
—. Returns a combined path from two paths.
DirectoryExists()
—. Returns true
when a directory exists.
FileExists()
—. Returns true
when a file exists.
GetCacheDependency()
—. Returns a cache dependency object that indicates when a file has been changed.
GetCacheKey()
—. Returns the key used by the cache dependency.
GetDirectory()
—. Returns a VirtualDirectory.
GetFileHash()
—. Returns a hash of the files used by the cache dependency.
OpenFile()
—. Returns the contents of a file.
Typically, you override the FileExists()
and GetFile()
methods to retrieve a file from your data store. If you want to represent directories, then you also need to override the DirectoryExists()
and GetDirectory()
methods.
Notice that several of these methods are related to caching. VirtualPathProvider
needs to know when a file has been modified so that it can retrieve the new version of the file and compile it. By default, the ASP.NET Framework uses a file dependency to determine when a file has been modified on the hard drive. However, in this situation a SqlCacheDependency
is used because the files will be stored in a database.
VirtualPathProvider
also includes a very useful property:
Previous
—. Returns the previously registered VirtualPathProvider
.
The Previous
property enables you to use the default VirtualPathProvider
. For example, if you want to store some files in the file system and other files in the database, then you can use the Previous
property to avoid rewriting all of the logic for working with files in the file system.
The GetFile()
method returns an instance of the VirtualFile
class. When using the VirtualPathProvider
, you must create a new class that inherits from the VirtualFile
class. This class contains the following properties:
IsDirectory
—. Always returns False
.
Name
—. Returns the name of the file.
VirtualPath
—. Returns the virtual path of the file.
The VirtualFile
class also contains the following method:
Open()
—. Returns the contents of the file.
Typically, when creating a class that inherits from the VirtualFile
class, you override the Open()
method. For example, we’ll override this method to get the contents of a file from a database table in the code sample built in this section.
The GetDirectory()
method returns an instance of the VirtualDirectory
class. This class contains the following properties:
Children
—. Returns all the files and directories that are children of the current directory.
Directories
—. Returns all the directories that are children of the current directory.
Files
—. Returns all the files that are children of the current directory.
IsDirectory
—. Always returns True
.
VirtualPath
—. Returns the virtual path of the directory.
There is another class in the ASP.NET Framework that you’ll want to use when working with the VirtualPathProvider
class. The VirtualPathUtility
class contains several useful methods for working with virtual paths:
AppendTrailingSlash()
—. Returns a path with at most one forward slash appended to the end of the path.
Combine()
—. Returns the combination of two virtual paths.
GetDirectory()
—. Returns the directory portion of a path.
GetExtension()
—. Returns the file extension of a path.
GetFileName()
—. Returns the file name from a path.
IsAbsolute()
—. Returns True
when a path starts with a forward slash.
IsAppRelative()
—. Returns True
when a path starts with a tilde (~).
MakeRelative()
—. Returns a relative path from an application-relative path.
RemoveTrailingSlash()
—. Removes trailing slash from the end of a path.
ToAbsolute()
—. Returns a path that starts with a forward slash.
ToAppRelative()
—. Returns a path that starts with a tilde (~).
By taking advantage of the VirtualPathUtility
class, you can avoid doing a lot of tedious string parsing on paths.
Before you can use an instance of the VirtualPathProvider
class, you must register it for your application. You can register a VirtualPathProvider
instance with the HostingEnvironment.RegisterVirtualPathProvider()
method.
You need to register VirtualPathProvider
when an application first initializes. You can do this by creating a shared method named AppInitialize()
and adding the method to any class contained in the App_Code folder. The AppInitialize()
method is automatically called by the ASP.NET Framework when an application starts.
For example, the following AppInitialize
method registers a VirtualPathProvider
named MyVirtualPathProvider
:
public static void AppInitialize() { MyVirtualPathProvider myProvider = new MyVirtualPathProvider(); HostingEnvironment.RegisterVirtualPathProvider(myProvider); }
In our VirtualPathProvider
application, we’ll include the AppInitialize()
method in the VirtualPathProvider
class itself.
In this section, we’ll create a VirtualPathProvider
that stores files and directories in two Microsoft SQL Server database tables named VirtualFiles
and VirtualDirectories
. The VirtualFiles table looks like this:
Path | Name | Content |
---|---|---|
|
| The time is now
|
|
| The first product |
|
| The second product |
The Path column represents the directory that contains the file. The Name column contains the name of the file. Finally, the Content column contains the actual file content.
Notice that the file can contain scripts. The Test.aspx
page displays the current date and time. You can place anything that you would place in a normal ASP.NET page, including ASP.NET controls, in the Content column.
The VirtualDirectories
table looks like this:
Path | ParentPath |
---|---|
~/ | NULL |
~/Products | ~/ |
The Path column represents the entire directory path. The ParentPath column represents the entire directory path of the directory that contains the directory.
The VirtualPathProvider
class in Listing 21.7—named SqlVirtualPathProvider
—uses both database tables.
Example 21.7. SqlVirtualPathProvider.cs
using System; using System.Web; using System.Web.Caching; using System.Collections; using System.Collections.Generic; using System.Web.Hosting; namespace AspNetUnleashed { public class SqlVirtualPathProvider : VirtualPathProvider { /// <summary> /// Register VirtualPathProvider for the application /// </summary> public static void AppInitialize() { SqlVirtualPathProvider sqlProvider = new SqlVirtualPathProvider(); HostingEnvironment.RegisterVirtualPathProvider(sqlProvider); } public SqlVirtualPathProvider() : base() { } /// <summary> /// Returns true when the file is a virtual file /// instead of a normal filesystem file /// </summary> private bool IsVirtualFile(string virtualPath) { String appVirtualPath = VirtualPathUtility.ToAppRelative(virtualPath); return !appVirtualPath.StartsWith("~/admin/", StringComparison.InvariantCultureIgnoreCase); } /// <summary> /// Returns true when a file exists /// </summary> public override bool FileExists(string virtualPath) { if (IsVirtualFile(virtualPath)) return VirtualFiles.FileExists(virtualPath); else return Previous.FileExists(virtualPath); } /// <summary> /// Gets a SqlVirtualFile which corresponds /// to a file with a certain path /// </summary> public override VirtualFile GetFile(string virtualPath) { if (IsVirtualFile(virtualPath)) return new SqlVirtualFile(virtualPath); else return Previous.GetFile(virtualPath); } /// <summary> /// Returns true when a directory exists /// </summary> public override bool DirectoryExists(string virtualPath) { if (IsVirtualFile(virtualPath)) return VirtualFiles.DirectoryExists(virtualPath); else return Previous.DirectoryExists(virtualPath); } /// <summary> /// Returns a SqlVirtualDirectory which corresponds /// to a virtual path /// </summary> public override VirtualDirectory GetDirectory(string virtualPath) { if (IsVirtualFile(virtualPath)) return new SqlVirtualDirectory(virtualPath); else return Previous.GetDirectory(virtualPath); } /// <summary> /// Gets the SqlCacheDependency object for the VirtualFilesDB /// database /// </summary> public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { if (IsVirtualFile(virtualPath)) return new SqlCacheDependency("VirtualFiles", "VirtualFiles"); else return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } } }
The class in Listing 21.7 overrides the FileExists()
, GetFile()
, DirectoryExists()
, and GetDirectory()
methods of the base VirtualPathProvider
class.
The class also includes a private method named IsVirtualFile()
. This method returns the value True
when a file is not contained in the Admin folder. The Admin directory contains a normal file system file. You’ll notice that each method, such as the FileExists()
method, checks the IsVirtualFile()
method. If the method returns False
, the Previous
property is used to pass the handling of the file to the file system.
The SqlVirtualPathProvider
class also overrides the GetCacheDependency()
method. This method returns a SqlCacheDependency
. The SQL cache dependency is configured with the Web configuration file in Listing 21.8.
Example 21.8. Web.Config
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <connectionStrings> <add name="VirtualFiles" connectionString="Data Source=.SQLExpress;Integrated Security=True;AttachDbFileName=|DataDirectory|VirtualFilesDB.mdf; User Instance=True"/> </connectionStrings> <system.web> <caching> <sqlCacheDependency enabled="true"> <databases> <add name="VirtualFiles" connectionStringName="VirtualFiles" pollTime="5000"/> </databases> </sqlCacheDependency> </caching> </system.web> </configuration>
To use the SQL cache dependency, you must configure the SQL database to support the cache dependency. You can enable SQL cache dependencies for the VirtualFilesDB database and the two database tables contained in the database by executing the following two commands from a Command Prompt after navigating to the application’s App_Data folder:
enableNotifications "VirtualFilesDB.mdf", "VirtualDirectories" enableNotifications "VirtualFilesDB.mdf", "VirtualFiles"
SQL cache dependencies are discussed in detail in Chapter 25, “Caching Application Pages and Data.”
The GetFile()
method in the SqlVirtualPathProvider
class returns an instance of the SqlVirtualFile
class. This class is contained in Listing 21.9.
Example 21.9. SqlVirtualFile.cs
using System; using System.Data; using System.Data.SqlClient; using System.Web.Hosting; using System.IO; using System.Web; namespace AspNetUnleashed { /// <summary> /// Summary description for SqlVirtualFile /// </summary> public class SqlVirtualFile : VirtualFile { public SqlVirtualFile(string virtualPath) : base(virtualPath){} public override Stream Open() { // Get content from database string content = VirtualFiles.FileContentSelect(this.VirtualPath); // return results as stream MemoryStream mem = new MemoryStream(); StreamWriter writer = new StreamWriter(mem); writer.Write(content); writer.Flush(); mem.Seek(0, SeekOrigin.Begin); return mem; } public string Content { get { return VirtualFiles.FileContentSelect(this.VirtualPath); } } } }
The SqlVirtualFile
class overrides the Open()
method of the base VirtualFile
class. The Open()
method grabs the contents of the file from the Content column of the VirtualFiles database table.
The GetDirectory()
method returns an instance of the SqlVirtualDirectory
class.
The SqlVirtualDirectory
class overrides three properties of the base VirtualDirectory
class: the Children
, Directories
, and Files
properties. These properties return files and subfolders from the VirtualFiles and VirtualDirectories database tables.
The VirtualPathProvider
classes use the VirtualFiles
class to interact with the SQL database. The VirtualFiles
class acts as the data access layer. The code for the VirtualFiles class is too long to contain in the body of this book. However, the complete source is included on the CD that accompanies this book.
The CD that accompanies this book includes an application named SqlVirtualPathProviderApp, which contains all the files discussed in this section. The application also includes an Admin folder with a Default.aspx page which enables you to add, edit, and delete virtual directories and files (see Figures 21.3 and 21.4). You can use this page to build an entire application that is stored in the database.
This chapter explored several advanced topics related to website navigation. In the first two sections, you learned how to map URLs from one path to another. In the first section, you learned how to configure remappings in the Web configuration file. In the second section, you learned how to build a custom HTTP module, which enables you to use wildcard matches when remapping a URL.
In the final section of this chapter, you learned how to abstract pages in your application from the file system by using the VirtualPathProvider
class. You saw the creation of an application that enables you to store application files in a Microsoft SQL Server database table.