这篇文章描述了一个完整的 ASP.NET 2.0 URL 重写方案 。这个方案使用正则表达式来定义重写规则并解决通过虚拟 URLs 访问页面产生回发事件的一些可能的困难 。
为什么要重写 URL ?
将 URL 重写方法应用到你的 ASP.Net 应用程序的两个主要原因是:可用性和可维护性 。
可用性
谁都知道,相对于难于辨认的带参数的长的查询路径,用户更喜欢一些短的、简洁的 URL 。任何时候,一个容易记住和敲入的路径比添加到收藏夹更有用 。其次,当一个浏览器的收藏夹不可用时,记住的地址总比在搜索引擎中输入关键字进行搜索,然后再查找要强的多 。比较下面的两个地址:
(1) |
http://www.somebloghost.com/Blogs/Posts.aspx?Year=2006&Month=12&Day=10 |
(2) |
http://www. somebloghost.com/Blogs/2006/12/10/ |
第一个 URL 包含了查询字符串;第二个URL包含的信息可以让用户清楚的看到他看的东西,它还可以使用户更容易的修改地址栏的内容,如:http://www.somehost.com/Blogs/2006/12/.
可维护性
在很多WEB应用程序中,开发人员经常会将页面从一个目录移到另一个目录,让我们假设一开始有两个可用页面: http://www.somebloghost.com/Info/Copyright.aspx 和 http://www.somebloghost.com/Support/Contacts.aspx,但是后来开发者将 Copyright.aspx 和 Contacts.aspx 移到了 Help 目录,用户收藏起来地址就需要重新定位 。这个问题虽然可以简单的用 Response.Redirect(new location) 来解决,但是如果有成百上千的页面呢?应用程序中就会包含大量的无效链接 。
使用 URL 重写,允许用户只需修改配置文件,这种方法可以让开发者将web应用程序逻辑结构与物理结构独立开来 。
ASP.NET 2.0 中的原有的URL 映射
ASP.NET 2.0 为 web 应用程序提供了一个开箱即用的映射静态 URL 的解决方案 。这个方案不用编写代码就可以在 web.config 中将旧的 URLs 映射到新的地址 。 要使用 URL 映射,只需在 web.config 文件的 system.web 节中创建一个新的 urlMappings 节 ,并添加要映射的地址 (“ '/ ”指向应用程序的根目录):
<urlMappings enabled="true">
<add url="'/Info/Copyright.aspx" mappedUrl="'/Help/Copyright.aspx" />
<add url="'/Support/Contacts.aspx" mappedUrl="'/Help/Contacts.aspx" />
</urlMappings>
这样,如果用户输入 http://www.somebloghost.com/Support/Contacts.aspx, 它将看到 http://www.somebloghost.com/Help/Contacts.aspx , 而他并不知道那个页已经移除 。
这个方案对于只有两个页面被移到其它位置的情况是足够的 。但它对有一打的需要重定位的页或者需要创建一个整洁的URL来说,它是不合适的 。另一个使用Asp.Net 的原有的URL映射技术的不太好的地方是:如果 Contacts.aspx 页包含的元素在回发到服务器时(这是非常可能的), 用户将会惊奇的发现地址 http://www.somebloghost.com/Support/Contacts.aspx 却变成了 http://www.somebloghost.com/Help/Contacts.aspx
。 这是因为ASP.NET 引擎用页面的实际地址修改了表单form 的 action 属性 ,所以表单就变成了下面的样子:
<form name="formTest" method="post"
action="http://www.simple-talk.com/Help/Contacts.aspx" id="formTest">
</form>
这样看来,URL 映射在ASP.NET 2.0 中几乎是无用的 。我们应当能够使用一个映射规则来指定一系列相似的 URL 。最好的解决方案就是使用正则表达式 ( Wikipedia 上可以查看概览,and 在 .NET 下的实现可以查看 MSDN), 但由于 ASP.NET 2.0 映射不支持正则表达式,所以我们需要开发一个内建到 URL 映射的不同的方案- URL 重写模块 。 最好的方法就是创建一个可重用的、简单的配置模块来实现,显然我们应创建一个 HTTP 模块 (关于 HTTP 模块的详细信息请查看 MSDN 杂志) 并在独立的程序集中实现 。要使这个程序集简单易用,我们应实现这个重写引擎的可配置性,即能够在 web.config 中指定规则 。
在开发过程中,我们应能使这个重写模块打开或关闭 (比如你有一个较难捕获的bug,而它可能是由不正确的重写模块引起的)这样在 web.config 中对重写模块的配置节进行打开或关闭就成为一个选择 。这样,在 web.config 中,一个配置节的示例如下:
<rewriteModule>
<rewriteOn>true</rewriteOn>
<rewriteRules>
<rule source="(\d+)/(\d+)/(\d+)/"
destination="Posts.aspx?Year=$1&Month=$2&Day=$3"/>
<rule source="(.*)/Default.aspx"
destination="Default.aspx?Folder=$1"/>
</rewriteRules>
</rewriteModule>
这样,所有像: http://localhost/Web/2006/12/10/ 这样的请示,将会在内部将会用带参数的请求重定向到 Posts.aspx 。
请注意: web.config 是一个结构良好的 XML 文件, 它禁止在属性值中使用 & 符号,所以在例子中,应当使用 & 代替 。
要在配置文件中使用这个重写模块,还需要注册节和指定处理模块,像下面这样增加一个configSections配置节:
<configSections> <sectionGroup name="modulesSection">
<section name="rewriteModule" type="RewriteModule.
RewriteModuleSectionHandler, RewriteModule"/>
</sectionGroup>
</configSections>
这样你就可以在 configSections 节的后面这样使用了:
<modulesSection>
<rewriteModule>
<rewriteOn>true</rewriteOn>
<rewriteRules>
<rule source="(\d+)/(\d+)/(\d+)/" destination="Post.aspx?Year=$1&Month=$2&Day=$3"/>
<rule source="(.*)/Default.aspx" destination="Default.aspx?Folder=$1"/>
</rewriteRules>
</rewriteModule>
</modulesSection>
另一个我们在开发重写模块过程中要做的就是还需要允许在虚拟路径中传递参数,象这样: http://www.somebloghost.com/2006/12/10/?Sort=Desc&SortBy=Date 。所以我们还需要有一个检测通过虚拟 URL 传递参数的解决方案 。

接下来让我们来创建类库 。首先,我们要引用 System.Web 程序集,这样我们可以实现一些基于 web 特殊功能 。如果要使我们的模块能够访问 web.config,还需要引用 System.Configuration 程序集 。
处理配置节
要能处理 web.config 中的配置,我们必需创建一个实现了 IConfigurationSectionHandler 接口的类 (详情查看 MSDN ) 。如下:
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Web;
using System.Xml;
namespace RewriteModule
{
public class RewriteModuleSectionHandler : IConfigurationSectionHandler
{
private XmlNode _XmlSection;
private string _RewriteBase;
private bool _RewriteOn;
public XmlNode XmlSection
{
get { return _XmlSection; }
}
public string RewriteBase
{
get { return _RewriteBase; }
}
public bool RewriteOn
{
get { return _RewriteOn; }
}
public object Create(object parent,
object configContext,
System.Xml.XmlNode section)
{
// set base path for rewriting module to
// application root
_RewriteBase = HttpContext.Current.Request.ApplicationPath + "/";
// process configuration section
// from web.config
try
{
_XmlSection = section;
_RewriteOn = Convert.ToBoolean(
section.SelectSingleNode("rewriteOn").InnerText);
}
catch (Exception ex)
{
throw (new Exception("Error while processing RewriteModule
configuration section.", ex));
}
return this;
}
}
}
RewriteModuleSectionHandler 类将在 web.config 中的 XmlNode 通过调用 Create 方法初始化 。XmlNode 类的 SelectSingleNode 方法被用来返回模块的配置值 。
使用重写的 URL 的参数
在处理象 http://www. somebloghost.com/Blogs/gaidar/?Sort=Asc (这是一个带参数的虚拟 URL ) 虚拟的 URLS 时,能够清楚的辨别通过虚拟路径传递的参数是非常重要的,如下:
<rule source="(.*)/Default.aspx" destination="Default.aspx?Folder=$1"/>,
你可能使用这样的 URL:
http://www. somebloghost.com/gaidar/?Folder=Blogs
它的效果和下面的相似:
http://www. somebloghost.com/Blogs/gaidar/
要处理这个问题,我们需要对虚拟路径参数 进行包装 。这可以是通过一个静态的方法去访问当前的参数集:
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Specialized;
using System.Web;
namespace RewriteModule
{
public class RewriteContext
{
// returns actual RewriteContext instance for
// current request
public static RewriteContext Current
{
get
{
// Look for RewriteContext instance in
// current HttpContext. If there is no RewriteContextInfo
// item then this means that rewrite module is turned off
if(HttpContext.Current.Items.Contains("RewriteContextInfo"))
return (RewriteContext)
HttpContext.Current.Items["RewriteContextInfo"];
else
return new RewriteContext();
}
}
public RewriteContext()
{
_Params = new NameValueCollection();
_InitialUrl = String.Empty;
}
public RewriteContext(NameValueCollection param, string url)
{
_InitialUrl = url;
_Params = new NameValueCollection(param);
}
private NameValueCollection _Params;
public NameValueCollection Params
{
get { return _Params; }
set { _Params = value; }
}
private string _InitialUrl;
public string InitialUrl
{
get { return _InitialUrl; }
set { _InitialUrl = value; }
}
}
}
可以看到,这样就可以通过RewriteContext.Current 集合来访问 “虚拟路径参数”了,所有的参数都被指定成了虚拟路径或页面,而不是像查询字符串那样了 。
重写 URL
接下来,让我们尝试重写 。首先,我们要读取配置文件中的重写规则 。其次,我们要检查那些在 URL 中与规则不符的部分,如果有,进行重写并以适当的页执行 。
创建一个 HttpModule:
class RewriteModule : IHttpModule
{
public void Dispose() { }
public void Init(HttpApplication context)
{}
}
当我们添加 RewriteModule_BeginRequest 方法以处理不符合规则的 URL时,我们要检查给定的 URL 是否包含参数,然后调用 HttpContext.Current.RewritePath 来进行控制并给出合适的 ASP.NET 页 。
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Configuration;
using System.Xml;
using System.Text.RegularExpressions;
using System.Web.UI;
using System.IO;
using System.Collections.Specialized;
namespace RewriteModule
{
class RewriteModule : IHttpModule
{
public void Dispose() { }
public void Init(HttpApplication context)
{
// it is necessary to
context.BeginRequest += new EventHandler(
RewriteModule_BeginRequest);
}
void RewriteModule_BeginRequest(object sender, EventArgs e)
{
RewriteModuleSectionHandler cfg =
(RewriteModuleSectionHandler)
ConfigurationManager.GetSection
("modulesSection/rewriteModule");
// module is turned off in web.config
if (!cfg.RewriteOn) return;
string path = HttpContext.Current.Request.Path;
// there us nothing to process
if (path.Length == 0) return;
// load rewriting rules from web.config
// and loop through rules collection until first match
XmlNode rules = cfg.XmlSection.SelectSingleNode("rewriteRules");
foreach (XmlNode xml in rules.SelectNodes("rule"))
{
try
{
Regex re = new Regex(
cfg.RewriteBase + xml.Attributes["source"].InnerText,
RegexOptions.IgnoreCase);
Match match = re.Match(path);
if (match.Success)
{
path = re.Replace(
path,
xml.Attributes["destination"].InnerText);
if (path.Length != 0)
{
// check for QueryString parameters
if(HttpContext.Current.Request.QueryString.Count != 0)
{
// if there are Query String papameters
// then append them to current path
string sign = (path.IndexOf(?) == -1) ? "?" : "&";
path = path + sign +
HttpContext.Current.Request.QueryString.ToString();
}
// new path to rewrite to
string rew = cfg.RewriteBase + path;
// save original path to HttpContext for further use
HttpContext.Current.Items.Add(
"OriginalUrl",
HttpContext.Current.Request.RawUrl);
// rewrite
HttpContext.Current.RewritePath(rew);
}
return;
}
}
catch (Exception ex)
{
throw (new Exception("Incorrect rule.", ex));
}
}
return;
}
}
}
这个方法必须注册:
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(RewriteModule_BeginRequest);
}
但这些仅仅完成了一半,因为重写模块还要处理表单的回发和虚拟路径参数集合,而这段代码中你会发现并没处理这些 。让我们先把虚拟路径参数放到一边,先来正确地处理最主要的回发 。
如果我们运行上面的代码,并通过查看 ASP.NET 的 HTML 源代码 的 action 会发现,它竟然包含了一个 ASP.NET 的实际路径页 。例如,我们使用页 '/Posts.aspx 来处理像 http://www. somebloghost.com/Blogs/2006/12/10/Default.aspx 的请求, 发现 action="/Posts.aspx" 。这意味着用户并没有使用虚拟路径进行回发,而是使用了实际的 http://www. somebloghost.com/Blog.aspx. 这个并不是我们需要的 。所以,需要加一段代码来处理这些不希望的结果 。
首先,我们要在 HttpModule 注册和实现一个另外的方法:
public void Init(HttpApplication context)
{
// it is necessary to
context.BeginRequest += new EventHandler(
RewriteModule_BeginRequest);
context.PreRequestHandlerExecute += new EventHandler(
RewriteModule_PreRequestHandlerExecute);
}
void RewriteModule_PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if ((app.Context.CurrentHandler is Page) &&
app.Context.CurrentHandler != null)
{
Page pg = (Page)app.Context.CurrentHandler;
pg.PreInit += new EventHandler(Page_PreInit);
}
}
这个方法检查用户是否请求了一个正常的 ASP.NET 页,然后为该页的 PreInit 事件增加处理过程 。这儿 RewriteContext 将处理实际参数,然后二次重写URL 。二次重写是必需的,以使 ASP.NET 能够在它的表单的action属性中使用一个虚拟路径 。
void Page_PreInit(object sender, EventArgs e)
{
// restore internal path to original
// this is required to handle postbacks
if (HttpContext.Current.Items.Contains("OriginalUrl"))
{
string path = (string)HttpContext.Current.Items["OriginalUrl"];
// save query string parameters to context
RewriteContext con = new RewriteContext(
HttpContext.Current.Request.QueryString, path);
HttpContext.Current.Items["RewriteContextInfo"] = con;
if (path.IndexOf("?") == -1)
path += "?";
HttpContext.Current.RewritePath(path);
}
}
最后,我们来看一下在我们的重写模块程序集中的三个类:

在 web.config 中注册重写模块
要使用重写模块,需要在配置文件中的 httpModules 节注册重写模块,如下:
<httpModules>
<add name="RewriteModule" type="RewriteModule.RewriteModule, RewriteModule"/>
</httpModules>
使用重写模块
在使用重写模块时,需要注意:
- 在 web.config 中来使用一些特殊字符是不可能的,因为它是一个结构良好的 XML 文件,因此,你只能用 HTML 编码的字符代替,如:使用 & 代替 & 。
- 要在你的 ASPX 中使用相对路径,需要在HTML标签调用 ResolveUrl 方法,如: <img src="<%=ResolveUrl("'/Images/Test.jpg")%>" /> 。
- Bear in mind the greediness of regular expressions and put rewriting rules to web.config in order of their greediness, for instance:
<rule source="Directory/(.*)/(.*)/(.*)/(.*).aspx"
destination="Directory/Item.aspx?
Source=$1&Year=$2&ValidTill=$3&Sales=$4"/>
<rule source="Directory/(.*)/(.*)/(.*).aspx"
destination="Directory/Items.aspx?
Source=$1&Year=$2&ValidTill=$3"/>
<rule source="Directory/(.*)/(.*).aspx"
destination="Directory/SourceYear.aspx?
Source=$1&Year=$2&"/>
<rule source="Directory/(.*).aspx"
destination="Directory/Source.aspx?Source=$1"/>
- 如果你要在页面中使用 RewriteModule 而不使用 .aspx,就必须在 IIS 中进行配置以使用期望的扩展映射到请求页,如下节所述:
IIS 配置: 使用带扩展的重写模块代替 .aspx
要使用带扩展的重写模块代替 .aspx (如 .html or .xml), 必须配置 IIS ,以使这些扩展映射到 ASP.NET 引擎 (ASP.NET ISAPI 扩展) 。要进行这些设置,需要以管理员身份登录 。
打开 IIS 管理控制台,并选择你要配置的站点的虚拟路径:
Windows XP (IIS 5)
Virtual Directory "RW"
Windows 2003 Server (IIS 6)
Default Web Site

然后在虚拟路径标签上点击 Configuration… 按钮 (或如果要使用整个站点都做映射就选择主目录标签) 。
Windows XP (IIS 5)

Windows 2003 Server (IIS 6)

接下来,点击添加按钮,并输入一个扩展,你还需要指定一个 ASP.NET ISAPI 扩展,注意去掉选项的对勾以检查文件是否存在 。

如果你要把所有的扩展都映射到 ASP.NET,对Windows XP上的 IIS 5 来说只需要设置 .* 到 ASP.NET ISAPI ,但对 IIS 6 就不一样了,点击“添加”然后指定 ASP.NET ISAPI 扩展 。

总结
现在,我们已经创建了一个简单的但非常强大的 ASP.NET 重写模块,它支持可基于正则表达式的 URLs 和页面回发,这个解决方案是容易实现的,并且提供给用户的例子也是可用的,它可以用简短的、整洁的URL来替代查询字符串参数 。 要使用这个模块,只需简单在你的应用程序中对 RewriteModule 进行引用,然后在 web.config 文件中添加几行代码以使你不想显示的 URL 通过正则表达式代替 。这个重写模块是很容易部署的,因为只需要在web.config中修改任何“虚拟”的URL即可,如果你需要进行测试,还可以对重写模块进行关闭 。
要想对重写模块有一个深入的了解,你可以查看本文提供的原代码 。我相信你会发现这是一个比ASP.NET提供的原始映射更好的体验 。