ASP.NET URL 重写解决方案
功能强大的 ASP.NET URL 重写和处理重写参数的解决方案。
引言
该解决方案为 ASP.NET 开发人员提供了一种可扩展且易于管理的方式来重写其应用程序中使用的 URL。该解决方案分为三个部分:
- URL 配置管理器
- 导航管理器
- 回发 (Postbacks)
允许配置重写规则。此功能已开发用于允许重写以及排除某些文件或文件夹和重定向。
这使开发人员能够管理网站的虚拟结构。提供的示例使用 XML 文件,但也可以轻松地更改为使用其他类型的数据源。我认为此模块使此 URL 重写示例在大多数其他可用示例中脱颖而出。它允许将文件夹定义为参数,开发人员可以请求这些参数。
我在大多数可用的重写解决方案中发现的一个问题是,它们会回发到重写后的页面,导致 URL 不一致。通过覆盖基类 HTMLForm,我们可以避免这种情况。
背景
URL 重写是一种方法,它允许以用户友好的方式显示页面的 URL,同时为开发人员提供所需的信息。目前有许多解决方案可用,从内置的 ASP.NET 重写到 IIS 的 ISAPI 过滤器。
为什么选择此解决方案?
在 ASP.NET 中实现重写有很多方法,但我发现很少有示例能够满足我在应用程序中所需的全部功能,这促使我开发了这篇文章。此解决方案具有以下特点:
- 无需对 IIS 或服务器进行任何配置更改
- 使用正则表达式轻松定义规则
- 支持基于文件、文件夹和正则表达式的排除
- 重定向功能
- 轻松访问提供的参数
这其中最棒的功能之一是无需考虑“真实”页面;每个页面都会被重写,导航功能提供对 URL 和查询字符串所有方面的访问。
1. URL 配置管理器
此模块的任务是管理与 URL 重写相关的规则。在本例中,它基于外部 XML 文件,但如果需要,也可以轻松地将其集成到 web.config 文件中。
一个示例配置文件如下:
<?xml version="1.0" encoding="utf-8" ?>
<urlconfiguration enabled="true" />
<excludepath url="~/testfolder/(.*)" />
<excludefile url="~/testpage.aspx" />
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
<rewrite url="~/(.*).aspx" newpath="~/default.aspx" />
</urlconfiguration />
此示例显示了可用的规则类型。下面将更详细地介绍这些规则。
<excludepath url="~/testfolder/(.*)" />
此规则定义了对 testfolder 中任何文件的请求不应被重写。这也将包括任何子文件夹中的文件。
<excludefile url="~/testpage.aspx" />
此规则规定,对位于应用程序根目录中的 testpage.aspx 文件的任何请求都将不会被重写。
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
重定向规则会将匹配指定标准的任何文件重写为 newPath
属性中定义的 URL。
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
重定向规则会绕过重写,并立即 Response.Redirect
到指定的 URL,该 URL 可以是站点内部或外部的页面。
<rewrite url="~/(.*).aspx" newpath="~/default.aspx" />
最后一条规则会将匹配标准的任何文件重写为由特定页面处理。例如,如果需要,可以将 /books/ 文件夹中的任何内容轻松更改为重写到 showbook.aspx。
现在我们有了配置文件,还需要创建几个类:一个用于读取数据,另一个类实现 IHTTPModule
,它将执行重定向例程。
首先,我们将创建一个 XMLConfigurationManager
类,该类从指定的配置文件中读取数据,以便提供给重定向类。
Public Class XMLConfigurationManager
Private _configurationPath As String
Public Property ConfigurationPath() As String
Get
Return _configurationPath
End Get
Set(ByVal value As String)
_configurationPath = value
End Set
End Property
Public Sub New(ByVal configurationDocumentPath As String)
ConfigurationPath = configurationDocumentPath
End Sub
Friend Function GetConfiguration() As XmlNode
'Ensure that the configuration path exists
If Not IO.File.Exists(ConfigurationPath) Then
Throw New Exception("Could not obtain configuration information")
End If
'Load the configuration settings
Dim settings As New XmlDocument
settings.Load(ConfigurationPath)
Return settings.ChildNodes(1)
End Function
End Class
此类有一个构造函数,它接受配置文件路径(可以存储在 web.config 文件中),并提供一个函数来返回根配置节点。
需要的第二个类是执行重写的 HTTP 模块;在这种情况下,它是一个名为 UrlRewritingModule
的类。
Public Class UrlRewritingModule
Implements System.Web.IHttpModule
'TODO: Insert class code here
End Class
这会将该类创建为一个 HTTP 模块。然后,我们可以将其添加到 web.config;我将在文章后面详细介绍。
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub
Public Sub Init(ByVal context As System.Web.HttpApplication)
Implements System.Web.IHttpModule.Init
If context Is Nothing Then
Throw New Exception("No context available")
End If
AddHandler context.AuthorizeRequest, AddressOf Me.rewrite
End Sub
上面显示的 Init
方法只是确保应用程序存在,并创建一个将在请求被授权后触发的事件处理程序。
Private Sub rewrite(ByVal sender As Object, ByVal e As EventArgs)
Dim app As HttpApplication = CType(sender, HttpApplication)
If app Is Nothing Then
Throw New Exception("No application available")
End If
Dim urlConfiguration As New XMLConfigurationManager _
(HttpContext.Current.Request.PhysicalApplicationPath + _
System.Configuration.ConfigurationManager.AppSettings("UrlDataRelativePath"))
Dim requestedPage As String = "~" + app.Request.RawUrl
Dim querystring As String = String.Empty
If requestedPage.Contains("?") Then
querystring = requestedPage.Substring(requestedPage.IndexOf("?") + 1)
requestedPage = requestedPage.Substring(0, requestedPage.IndexOf("?"))
End If
Dim newUrl As String = getRewrittenUrl(requestedPage,
urlConfiguration.GetConfiguration)
If Not String.IsNullOrEmpty(querystring) Then
newUrl += "?" + querystring
End If
HttpContext.Current.RewritePath(newUrl, False)
End Sub
此方法首先使用从 web.config 文件获取的文件路径,从 XMLConfigurationManager
类读取配置。然后,在将请求的 URL 传递给 getRewrittenUrl
函数的同时,将其从查询字符串中分离出来。完成后,将查询字符串重新附加到 URL,并使用 ASP.NET 的 httpcontext.current.rewritepath
函数重写页面。
URL 配置的最后一部分是根据提供的规则返回重写 URL 的函数。
Private Function getRewrittenUrl(ByVal requestedPage As String, _
ByVal configurationNode As XmlNode) As String
'If the rewriting is disabled then return the source url
If CType(configurationNode.Attributes("enabled").Value, Boolean) = False Then
Return requestedPage
End If
'Iterate though the configuration document looking for a match
For Each childNode As XmlNode In configurationNode.ChildNodes
Dim regEx As New Regex(childNode.Attributes("url").Value, _
RegexOptions.IgnoreCase)
If regEx.Match(requestedPage).Success Then
Select Case childNode.Name
Case "excludePath", "excludeFile"
Return requestedPage
Case "rewrite"
Return childNode.Attributes("newPath").Value
Case "redirect"
HttpContext.Current.Response.Redirect _
(childNode.Attributes("newPath").Value)
End Select
End If
Next
Return requestedPage
End Function
上面的代码遍历规则,并根据节点的名称执行所需的功能,该功能在排除的情况下返回原始 URL,将请求重定向到指定页面,或将 URL 重写到指定的目录。
2. 导航管理器
导航管理器是位于 URL 重写之上的功能,它为开发人员提供有关当前页面的信息。此模块同样使用外部 XML 文件,但我认为在许多情况下,最好使用数据库。因此,我实现了一个基类和一个基于 XML 的管理器;可以使用类似的方法轻松开发 SQL/OLEDB 或其他提供程序。
理论
导航的设计方式是允许网站内存在固定和“动态”的文件夹结构。任何动态文件夹都可以作为键值对暴露给应用程序。例如,URL /products/shoes/nike/shoes.aspx。
- /products/ - 用于分隔站点功能区域的固定文件夹。
- /shoes/ - 可能是类别。这是一个动态值,开发人员需要能够向用户显示信息。
- /nike/ - 制造商。同样是动态文件夹。
- /shoes.aspx - 页面名称。
导航管理器允许在代码中请求当前类别、制造商或正在查看的页面;例如:
Dim currentManufacturer as string = provider.GetNavigationValue("manufacturer")
或
Dim pageName as string = provider.GetNavigationValue("CurrentPage")
配置文件
此文件创建网站导航的虚拟结构。本示例已从我当前的 SQL 解决方案转换为 XML,以便于撰写本文。
<?xml version="1.0" encoding="utf-8" ?>
<Navigation>
<Folder Name="Products" Value="products" IsDynamic="False">
<Folder Name="Category" Value="productCategory" IsDynamic ="True" />
</Folder>
<Folder Name="About" Value="about" IsDynamic="False" />
<Folder Name="Articles" Value="articles" IsDynamic="False">
<Folder Name="Subject" Value="articleSubject" IsDynamic="true">
<Folder Name="Year" Value="year" IsDynamic="true" />
</Folder>
</Folder>
</Navigation>
正如您所见,这是一个简单的文件夹结构,其中定义了文件夹是动态还是静态。一个限制是每个文件夹只能包含一个动态文件夹;否则,应用程序将无法确定使用哪个键。
Name
属性指定了一个更友好的文件夹名称,可能用于面包屑或在站点上显示。Value
属性是在尝试获取值时暴露给开发人员的键名,而 IsDynamic
指定文件夹是否为动态(显而易见)。
NavigationProviderBase
此基类包含管理导航的基本逻辑。
Public MustInherit Class NavigationProviderBase
Protected MustOverride Sub processNavigation()
Private _navigationParts As New Dictionary(Of String, String)
Friend Property NavigationParts() As Dictionary(Of String, String)
Get
Return _navigationParts
End Get
Set(ByVal value As Dictionary(Of String, String))
_navigationParts = value
End Set
End Property
Private _querystringParts As New Dictionary(Of String, Object)
Friend Property QuerystringParts() As Dictionary(Of String, Object)
Get
Return _querystringParts
End Get
Set(ByVal value As Dictionary(Of String, Object))
_querystringParts = value
End Set
End Property
Private _currentUrl As String
Public Property CurrentUrl() As String
Get
Return _currentUrl
End Get
Set(ByVal value As String)
_currentUrl = value
End Set
End Property
Public Sub New()
Dim querystring As String = String.Empty
CurrentUrl = HttpContext.Current.Request.RawUrl
'Remove the domain and querystring information
If HttpContext.Current.Request.RawUrl.Contains("?") Then
CurrentUrl = HttpContext.Current.Request.RawUrl.Substring _
(0, CurrentUrl.IndexOf("?"))
End If
'Add the querystring values to the dictionary
processQuerystring()
End Sub
Private Sub processQuerystring()
'Convert the querstring items to an array
For Each key As String In HttpContext.Current.Request.QueryString.Keys
QuerystringParts.Add(key, HttpContext.Current.Request.QueryString(key))
Next
End Sub
End Class
基类包含创建查询字符串值的信息,这些值可以与导航值一起提供给页面(导航值在另一个类中进行解释,以便使用不同的数据源)。查询字符串在此类中的实现是一个附加功能,但对应用程序而言并非必不可少。
XMLNavigationProvider
此类继承自 NavigationProviderBase
(如上),其任务是从 XML 文件中读取数据到页面可用的键值对列表中。
Public Class XMLNavigationProvider
Inherits NavigationProviderBase
Private _configurationPath As String
Public ReadOnly Property ConfigurationPath() As String
Get
Return _configurationPath
End Get
End Property
Public Sub New(ByVal configurationPath As String)
MyBase.New()
_configurationPath = configurationPath
processNavigation()
End Sub
Protected Overrides Sub processNavigation()
'Create an array to contain the raw information regarding the Url
Dim items() As String = CurrentUrl.Split(New Char() {"/"c}, _
StringSplitOptions.RemoveEmptyEntries)
'Load the XML navigation structure
'Ensure that the configuration path exists
If Not IO.File.Exists(ConfigurationPath) Then
Throw New Exception("Could not obtain configuration information")
End If
'Load the configuration settings
Dim settings As New XmlDocument
settings.Load(ConfigurationPath)
ProcessNode(items, settings.ChildNodes(1), 0)
End Sub
Protected Sub ProcessNode(ByVal items() As String, _
ByVal parentNode As XmlNode, ByVal currentLevel As Integer)
'Iterate though the items, build up the navigation keys
If currentLevel >= items.Length Then Return
If items(currentLevel).ToLower.Contains(".aspx") Then
NavigationParts.Add("currentPage", _
items(currentLevel).Replace(".aspx", ""))
End If
Dim currentFolder As XmlNode = Nothing
For Each item As XmlNode In parentNode.ChildNodes
If CType(item.Attributes("IsDynamic").Value, Boolean) _
And Not items(currentLevel).ToLower.Contains(".aspx") Then
currentFolder = item
Else
If CType(item.Attributes("Value").Value, String).ToLower _
= items(currentLevel).ToLower Then
currentFolder = item
End If
End If
Next
If currentFolder IsNot Nothing Then
NavigationParts.Add(CType(currentFolder.Attributes("Value").Value _
, String).ToLower, items(currentLevel))
ProcessNode(items, currentFolder, currentLevel + 1)
End If
End Sub
End Class
代码主要围绕一个主函数,该函数遍历导航部分(本质上是文件夹)并将这些值映射到配置文件中包含的键。然后将它们添加到 NavigationParts
集合中。
Web.config 更改
需要对 web.config 文件进行一些更改才能启用 URL 重写。我们需要定义导航和 URL 配置文件以及添加 HTTP 模块的路径。
<appsettings />
<add value="/config/URLConfiguration.xml" key="UrlDataRelativePath" />
<add value="/config/NavigationConfiguration.xml" key="NavigationDataRelativePath" />
</appsettings />
并在 system.web
节点内部,我们需要:
<httpmodules />
<add name="UrlRewritingModule" type="Rewriting.UrlRewritingModule" />
</httpmodules />
处理回发
此时,您将拥有一个可以正确重写 URL 的解决方案,但在回发时,应用程序将返回到重写的页面,这不是期望的功能。为了解决这个问题,我们可以创建一个自定义 HTMLForm,以确保回发发生在重写的页面上。
Public Class FormRewriterControlAdapter
Inherits System.Web.UI.Adapters.ControlAdapter
Protected Overrides Sub Render(ByVal writer As _
System.Web.UI.HtmlTextWriter)
MyBase.Render(New RewriteFormHtmlTextWriter(writer))
End Sub
End Class
Public Class RewriteFormHtmlTextWriter
Inherits HtmlTextWriter
Sub New(ByVal writer As HtmlTextWriter)
MyBase.New(writer)
Me.InnerWriter = writer.InnerWriter
End Sub
Sub New(ByVal writer As System.IO.TextWriter)
MyBase.New(writer)
MyBase.InnerWriter = writer
End Sub
Public Overrides Sub WriteAttribute(ByVal name As String, _
ByVal value As String, ByVal fEncode As Boolean)
If (name = "action") Then
Dim Context As HttpContext
Context = HttpContext.Current
If Context.Items("ActionAlreadyWritten") Is Nothing Then
value = Context.Request.RawUrl
Context.Items("ActionAlreadyWritten") = True
End If
End If
MyBase.WriteAttribute(name, value, fEncode)
End Sub
End Class
然后,您可以通过将一个 Form.browser 文件添加到 App_Browsers 文件夹中,并包含以下文本来实现:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.HtmlControls.HtmlForm"
adapterType="URLRewriting.Rewriting.FormRewriterControlAdapter" />
</controlAdapters>
</browser>
</browsers>
来源
请下载上面链接的完整解决方案,以获取源代码和一个简单的测试应用程序。
历史
- 2007 年 7 月 2 日:在Andrei Rinea发表评论后,我已更新代码,使其不再使用继承的表单来处理回发,因为这会破坏设计时视图,而是使用自定义的
ControlAdapter
。我在 Scott Gu 的博客上找到了这段代码,并将其集成到此解决方案中。
此解决方案的设计力求通用,实际上可能无法满足您应用程序的所有需求。我很乐意提出任何可能有帮助的更改。