在 ASP.NET 中使用 HTTPHandler 实现 Front Controller

来源:互联网 发布:ios软件已过期 编辑:程序博客网 时间:2024/04/25 23:00
 

在 ASP.NET 中使用 HTTPHandler 实现 Front Controller

发布日期: 4/1/2004 | 更新日期: 4/15/2004

使用 Microsoft .NET 的企业解决方案模式 > Web 表示模式 > 在 ASP.NET 中使用 HTTPHandler 实现 Front Controller

版本: 1.0.1

本页内容
上下文 上下文 背景信息 背景信息 实现策略 实现策略 测试考虑事项 测试考虑事项 结果上下文 结果上下文 相关模式 相关模式 致谢 致谢

上下文

您要在 ASP.NET 中构建 Web 应用程序。您已经评估了Page Controller (页面控制器)和Front Controller(前端控制器) 中描述的可选设计,并且已确定您的应用程序具有足够高的复杂程度需要实现前端控制器。

返回页首返回页首

背景信息

示例有助于解释如何在 ASP.NET 中实现 Front Controller,并让您看到通过单个控制器对象来集中所有控制所带来的价值,只要该示例的复杂程度足以说明您在实现该模式时将遇到的问题。

注意:因为 Page Controller 内置在 ASP.NET 中,所以,实现 Front Controller 而不是 Page Controller 就需要大量的工作。实际上,您必须为 Front Controller 构建整个框架。只有在您的应用程序具有足够高的复杂程度时,才应该这样做。否则,请阅读 Page Controller 相关内容,以确定它是否够用。

以下示例建立 ASP.NET 中实现 Page Controller中所描述的解决方案上。该解决方案描述了两个不同的页面。这两个页面是从一个公共的基类继承而来的,该基类负责将站点头信息添加到每个页面上。当您希望让页面具有相同行为时,该实现是 Page Controller 的常见选择。下面是 Page Controller 示例中的 BasePage 类:

using System; using System.Web.UI; using System.Web.UI.WebControls; public class BasePage : Page {    protected Label eMail;    protected Label siteName;    virtual protected void PageLoadEvent(object sender, System.EventArgs e)    {}    protected void Page_Load(object sender, System.EventArgs e)    {       if(!IsPostBack)       {          string name = Context.User.Identity.Name;          eMail.Text = DatabaseGateway.RetrieveAddress(name);          siteName.Text = "Micro-site";          PageLoadEvent(sender, e);       }    }    #region Web Form Designer generated code    override protected void OnInit(EventArgs e)    {       //       // CODEGEN: This call is required by the ASP.NET Web Form Designer.       //       InitializeComponent();       base.OnInit(e);    }    //     // 设计器支持所必需的方法 – 不要使用代码编辑器修改    // 此方法的内容。    //     private void InitializeComponent()    {           this.Load += new System.EventHandler(this.Page_Load);    }    #endregion } 

每次加载页面时,就会调用 Page_Load 函数。该函数从 DatabaseGateway 类检索电子邮件地址(如“在 ASP.NET 中实现 Page Controller”中所示),并用数据设置某些标签,然后调用 PageLoadEvent 对每个页面进行特殊化处理。

选择 Front Controller 而不是 Page Controller 的条件之一是,基类中的条件逻辑过多。此示例不使用基类中的条件逻辑。因此,单就此条件而言,无需实现 Front Controller

更改需求

前面的示例非常适用于其预期目的。不过,该示例过于简单,无法代表大多数 Web 应用程序。为了更接近于这类应用程序的整体复杂性,按照此示例的需求,我们需要在页面上使用不同的头信息(这取决于 URL 和查询参数)。

此示例将创建两个站点:Micro 站点和 Macro 站点。每个站点都查询不同的数据,以检索头信息中包含的电子邮件地址。页面本身保持不变;只有头信息内容是不同的。在此示例中,大多数实现与前面的示例相同。唯一必须修改的类是 BasePage

using System; using System.Web.UI; using System.Web.UI.WebControls; public class BasePage : Page {    protected Label eMail;    protected Label siteName;    virtual protected void PageLoadEvent(object sender, System.EventArgs e)    {}    protected void Page_Load(object sender, System.EventArgs e)    {       if(!IsPostBack)       {          string site = Request["site"];          if(site != null && site.Equals("macro"))             LoadMacroHeader();          else             LoadMicroHeader();          PageLoadEvent(sender, e);       }    }    private void LoadMicroHeader()    {       string name = Context.User.Identity.Name;       eMail.Text = WebUsersDatabase.RetrieveAddress(name);                   siteName.Text = "Micro-site";    }    private void LoadMacroHeader()    {       string name = Context.User.Identity.Name;       eMail.Text = MacroUsersDatabase.RetrieveAddress(name);                   siteName.Text = "Macro-site";    }    #region Web Form Designer generated code    override protected void OnInit(EventArgs e)    {       //       // CODEGEN: This call is required by the ASP.NET Web Form Designer.       //       InitializeComponent();       base.OnInit(e);    }    //     // Required method for Designer support - do not modify    // the contents of this method with the code editor.    //     private void InitializeComponent()    {           this.Load += new System.EventHandler(this.Page_Load);    }    #endregion } 

前面已经提到,Micro 站点和 Macro 站点各自使用不同的数据库来检索头信息中包含的电子邮件地址。LoadMacroHeaderLoadMicroHeader 这两个方法使用不同的数据库网关类,即 WebUsersDatabaseMacroUsersDatabase,来从数据库检索地址。

Page_Load 方法的职责已更改。在前面的示例中,它从数据库检索信息。在此实现中,它决定要调用哪个函数,LoadMicroHeader 还是 LoadMacroHeader,然后再调用适当的方法。如果只有两个站点,该实现就已足够了。不过,基类现在包含条件逻辑。您也许不希望看到此类中包含的逻辑。显然,大多数开发人员在看到代码中的分支较多时会觉得很麻烦,但两个分支可能不会让他们有这样的感觉。限制条件逻辑的主要原因是,条件逻辑更有可能发生更改,从而导致您修改实现。因为整个实现包含在一个文件中,因此,所做的更改会影响其他站点。

返回页首返回页首

实现策略

Front Controller 通常分为两个部分来实现。Handler 对象从 Web 服务器接收各个请求(HTTP Get 和 Post),并检索相关参数,然后根据参数选择适当的命令。控制器的第二个部分是 Command Processor,该部分执行特定操作或命令来满足请求。命令完成后转到视图,以便显示页面。

注意:此实现策略解决了前面的示例中出现的问题。虽然此示例可能不足以证明对 Front Controller 的更改是合理的,但它说明了为什么会使用 Front Controller ,并且该实现解决了这种复杂性高得多的问题。另外,与大多数实现一样,实现此模式的方式不止一种,这只是其中的一个选择。

处理程序

ASP.NET 提供低级请求/响应 API 来处理传入的 HTTP 请求。ASP.NET 所接收的每个传入 HTTP 请求最终由实现 IHTTPHandler 接口的类的具体实例来处理。这种低级 API 非常适用于实现 Front Controller 的处理程序部分。

注意:Microsoft? .NET Framework 为 HTTP 处理程序提供了多个实现选择。例如,在高容量环境中,您可以通过实现了 IHttpAsyncHandler 接口的异步 HTTP 处理程序来提高响应速度。为简单起见,此解决方案使用同步处理程序。有关如何实现异步 HTTP 处理程序的详细信息,请访问 Microsoft Developer Network (MSDN?) 网站 (http://msdn.microsoft.com)。

图 1 显示了控制器的处理程序部分的结构。


1 Front Controller 的处理程序部分

此解决方案完美地划分了职责。Handler 类负责处理各个 Web 请求,并将确定正确的 Command 对象这一职责委派给 CommandFactory 类。当 CommandFactory 返回 Command 对象后,Handler 将调用 Command 上的 Execute 方法来执行请求。

Handler.cs

下面的代码示例显示了如何实现 Handler 类:

using System; using System.Web; public class Handler : IHttpHandler {    public void ProcessRequest(HttpContext context)     {       Command command =           CommandFactory.Make(context.Request.Params);       command.Execute(context);    }    public bool IsReusable     {        get { return true;}     } } 

Command.cs

Command 类是 Command 模式 [Gamma95] 的一个示例。Command 模式在此解决方案中非常有用,因为您不希望 Handler 类直接依赖于命令。一般来说,可以从 CommandFactory 返回命令对象。

using System; using System.Web; public interface Command {    void Execute(HttpContext context); } 

CommandFactory.cs

CommandFactory 类对于实现至关重要。它根据查询字符串中的参数来判断将创建哪个命令。在此示例中,如果 site 查询参数被设置为 micro 或根本没有设置,工厂将创建 MicroSite 命令对象。如果 site 被设置为 macro,工厂将创建 MacroSite 命令对象。如果该值被设置为任何其他值,工厂将返回 UnknownCommand 对象,以便进行默认错误处理。这是 Special Case 模式 [Fowler03] 的一个示例。

using System; using System.Collections.Specialized; public class CommandFactory {    public static Command Make(NameValueCollection parms)    {       string siteName = parms["site"];       Command command = new UnknownCommand();       if(siteName == null || siteName.Equals("micro"))          command = new MicroSite();       else if(siteName.Equals("macro"))          command = new MacroSite();       return command;    } } 

配置处理程序

HTTP 处理程序在 ASP.NET 配置中被声明为 web.config 文件。ASP.NET 定义了一个可以在其中添加和删除处理程序的 <httphandlers> 配置段。例如,ASP.NET 将 Page*.aspx 文件的所有请求映射到应用程序的 web.config 文件中的 Handler 类:

<httpHandlers>    <add verb="*" path="Page*.aspx" type="Handler,FrontController" /> </httpHandlers> 

命令

命令代表了网站中的可变性。在此示例中,从每个站点的数据库中检索数据的功能包含在它自己的类中,并且该类是从名为 RedirectingCommand 的基类继承而来的。RedirectingCommand 类实现了 Command 接口。调用 RedirectingCommand 类的 Execute 时,它首先调用名为 OnExecute 的抽象方法,然后转到视图。该特定视图是从名为 UrlMap 的类检索而来的。UrlMap 类从应用程序的 web.config 文件中检索映射关系。图 2 显示了该解决方案的命令部分的结构。


2 front controller的命令部分

RedirectingCommand.cs

RedirectingCommand 是一个抽象基类,它调用名为 OnExecute 的抽象方法来执行特定命令,然后转到从 UrlMap 检索到的视图。

using System; using System.Web; public abstract class RedirectingCommand : Command {    private UrlMap map = UrlMap.SoleInstance;    protected abstract void OnExecute(HttpContext context);    public void Execute(HttpContext context)    {       OnExecute(context);       string url = String.Format("{0}?{1}",          map.Map[context.Request.Url.AbsolutePath],          context.Request.Url.Query);       context.Server.Transfer(url);    } } 

UrlMap.cs

UrlMap 类从应用程序的 web.config 文件加载配置信息。配置信息将所请求的 URL 的绝对路径关联到该文件所指定的另一个 URL。这样,就可以更改当请求外部页面时要将用户转到哪个实际页面。这个过程为更改视图提供了很高的灵活性,因为用户永远不会引用实际页面。下面是 UrlMap 类:

using System; using System.Web; using System.Xml; using System.Configuration; using System.Collections.Specialized; public class UrlMap : IConfigurationSectionHandler  {    private readonly NameValueCollection _commands = new NameValueCollection();    public const string SECTION_NAME="controller.mapping";    public static UrlMap SoleInstance     {       get {return (UrlMap) ConfigurationSettings.GetConfig(SECTION_NAME);}    }    object IConfigurationSectionHandler.Create(object parent,object configContext, XmlNode section)     {       return (object) new UrlMap(parent,configContext, section);       }    private UrlMap() {/*no-op*/}    public UrlMap(object parent,object configContext, XmlNode section)     {       try        {          XmlElement entriesElement = section["entries"];          foreach(XmlElement element in entriesElement)           {             _commands.Add(element.Attributes["key"].Value,element.Attributes["url"].Value);          }       }        catch (Exception ex)        {          throw new ConfigurationException("Error while parsing configuration section.",ex,section);       }    }       public NameValueCollection Map    {       get { return _commands; }    } } 

下面的代码是从显示配置的 web.config 文件中摘录的:

<controller.mapping>    <entries>       <entry key="/patterns/frontc/3/Page1.aspx" url="ActualPage1.aspx" />       <entry key="/patterns/frontc/3/Page2.aspx" url="ActualPage2.aspx" />    </entries> </controller.mapping>  

MicroSite.cs

MicroSite 类与此模式前面的 LoadMicroHeader 中的代码类似。主要区别是,无法再访问页面中包含的标签。而必须将信息添加到 HttpContext 对象。下面的示例显示了 MicroSite 代码:

using System; using System.Web; public class MicroSite : RedirectingCommand {    protected override void OnExecute(HttpContext context)    {       string name = context.User.Identity.Name;       context.Items["address"] =           WebUsersDatabase.RetrieveAddress(name);       context.Items["site"] = "Micro-Site";    } } 

MacroSite.cs

MacroSite 类与 MicroSite 类似,但它使用的是不同的数据库网关类 MacroUsersDatabase。这两个类都将信息存储在传递进来的 HttpContext 中,以便让视图可以检索它。下面的示例显示了 MacroSite 代码:

using System; using System.Web; public class MacroSite : RedirectingCommand {    protected override void OnExecute(HttpContext context)    {       string name = context.User.Identity.Name;       context.Items["address"] =           MacroUsersDatabase.RetrieveAddress(name);       context.Items["site"] = "Macro-Site";    } } 

WebUsersDatabase.cs

WebUsersDatabase 类负责从“webusers”数据库中检索电子邮件地址。它是 Table Data Gateway [Fowler03] 模式的一个示例。

using System; using System.Data; using System.Data.SqlClient; public class WebUsersDatabase {    public static string RetrieveAddress(string name)    {       string address = null;       String selectCmd =           String.Format("select * from webuser where (id = '{0}')",          name);       SqlConnection myConnection =           new SqlConnection("server=(local);database=webusers;Trusted_Connection=yes");       SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);       DataSet ds = new DataSet();       myCommand.Fill(ds,"webuser");       if(ds.Tables["webuser"].Rows.Count == 1)       {          DataRow row = ds.Tables["webuser"].Rows[0];          address = row["address"].ToString();       }       return address;    } } 

MacroUsersDatabase.cs

MacroUsersDatabase 类负责从“macrousers”数据库中检索电子邮件地址。它是 Table Data Gateway 模式的一个示例。

using System; using System.Data; using System.Data.SqlClient; public class MacroUsersDatabase {    public static string RetrieveAddress(string name)    {       string address = null;       String selectCmd =           String.Format("select * from customer where (id = '{0}')",          name);       SqlConnection myConnection =           new SqlConnection("server=(local);database=macrousers;Trusted_Connection=yes");       SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);       DataSet ds = new DataSet();       myCommand.Fill(ds,"customer");       if(ds.Tables["customer"].Rows.Count == 1)       {          DataRow row = ds.Tables["customer"].Rows[0];          address = row["email"].ToString();       }       return address;    } } 

视图

视图最后实现。“更改需求”中的示例视图负责根据用户访问哪个站点从数据库中检索信息,然后向用户显示所产生的页面。因为数据库访问代码已移到命令,所以视图现在从 ttpContext 对象检索数据。图 3 显示了代码隐藏类的结构。


3 视图的代码隐藏类的结构

由于仍然存在公共行为,因此仍然需要 BasePage 类以避免代码重复。

BasePage.cs

与“更改需要”中的示例相比,BasePage 类已有大幅更改。它不再负责确定要加载哪个站点头信息。它只检索由命令存储在 HttpContext 对象中的数据,并将它们分配给适当的标签:

using System; using System.Web.UI; using System.Web.UI.WebControls; public class BasePage : Page {    protected Label eMail;    protected Label siteName;    virtual protected void PageLoadEvent(object sender, System.EventArgs e)    {}    protected void Page_Load(object sender, System.EventArgs e)    {       if(!IsPostBack)       {          eMail.Text = (string)Context.Items["address"];          siteName.Text = (string)Context.Items["site"];          PageLoadEvent(sender, e);       }    }    #region Web Form Designer generated code    #endregion } 

ActualPage1.aspx.cs ActualPage2.aspx

ActualPage1ActualPage2 是针对具体页面的代码隐藏类。它们都是从 BasePage 继承而来的,以确保在屏幕的顶部填入头信息:

using System; using System.Web.UI; using System.Web.UI.WebControls; public class ActualPage1 : BasePage {    protected System.Web.UI.WebControls.Label pageNumber;    protected override void PageLoadEvent(object sender, System.EventArgs e)    {       pageNumber.Text = "1";    }    #region Web Form Designer generated code    #endregion } using System; using System.Web.UI.WebControls; public class ActualPage2 : BasePage {    protected Label pageNumber;    protected override void PageLoadEvent(object sender, System.EventArgs e)    {       pageNumber.Text = "2";    }   #region Web Form Designer generated code    #endregion } 

在从 Page Controller 实现转移到 Front Controller 实现时,不必更改这些页面。

返回页首返回页首

测试考虑事项

实现对 ASP.NET 运行库的依赖性使测试变得很困难。您无法将通过继承 System.Web.UI.PageSystem.Web.UI.IHTTPHandler 或 ASP.NET 运行库中所包含的其他各种类而得到的类进行实例化。这就无法对应用程序的大多数组成部分分别进行单元测试。自动测试此实现的所选方法是,生成 HTTP 请求,然后检索 HTTP 响应,并确定响应是否正确。此方法容易产生错误,因为这是在将响应文本与预期文本进行比较。

CommandFixture.cs

对于可测试的实现来说,导致其可测试的一个因素是 CommandFactory,因为它是独立于 ASP.NET 运行库的。因此,您可以通过编写测试步骤来验证是否获得了正确的 Command 对象。下面是 CommandFactory 类的 NUnit (http://nunit.org) 测试:

using System; using System.Collections.Specialized; using NUnit.Framework; [TestFixture] public class CommandFixture {    private static readonly string microKey = "micro";    private static readonly string macroKey = "macro";    [SetUp]    public void BuildCommandFactory()    {       NameValueCollection map = new NameValueCollection();       map.Add(microKey, "MicroSite");       map.Add(macroKey, "MacroSite");    }    [Test]    public void DefaultToMicro()    {       NameValueCollection map = new NameValueCollection();       Command command = CommandFactory.Make(map);       Assertion.AssertNotNull(command);       Assertion.Assert(command is MicroSite);    }    [Test]    public void MicroSiteCommand()    {       NameValueCollection map = new NameValueCollection();       map.Add("site", "micro");       Command command = CommandFactory.Make(map);       Assertion.AssertNotNull(command);       Assertion.Assert(command is MicroSite);    }    [Test]    public void MacroSiteCommand()    {       NameValueCollection map = new NameValueCollection();       map.Add("site", "macro");       Command command = CommandFactory.Make(map);       Assertion.AssertNotNull(command);       Assertion.Assert(command is MacroSite);    }    [Test]    public void Error()    {       NameValueCollection map = new NameValueCollection();       map.Add("site", "xyzcommand");       Command command = CommandFactory.Make(map);       Assertion.AssertNotNull(command);       Assertion.Assert(command is UnknownCommand);    } } 

可以通过进一步的工作来隔离 Command 类。Execute 方法的一个参数是 HttpContext 对象。您可以更改此参数,使该对象独立于 ASP.NET 环境。这样,您就可以在 ASP.NET 运行库之外对命令进行单元测试。

返回页首返回页首

结果上下文

实现 Front Controller 增加了复杂性,并导致了许多优缺点:

优点

提高了灵活性。该实现展示了如何通过 Handler 类集中和协调所有请求。Handler 使用 CommandFactory 来确定要执行的具体操作。这样,就可以在不更改 Handler 类的情况下修改和扩展功能。例如,要添加另一个站点,则必须创建特定命令,并且唯一必须更改的类是 CommandFactory

简化了视图。Page Controller 示例中的视图从数据库检索数据,然后产生页面。在 Front Controller 中,视图不必再依赖数据库,因为这项工作是由各个命令来完成的。

可以扩展,但不能修改。该实现为进行多种形式的调度提供了许多机会。例如,无论执行什么方法和对象,Handler 只调用 Command 对象的 Execute 方法。因此,您可以在不修改 Handler 的情况下添加额外的命令。通过用其他工厂代替 CommandFactory,可以对该实现进行进一步扩展。

URL 映射UrlMap 允许让用户看不到实际的页面名。用户输入一个 URL,然后系统将使用 web.config 文件将它映射到特定的 URL。这可以让程序员有更大的灵活性,因为这样做可以获得 Page Controller 实现中所没有的一个间接操作层。

线程安全。命令对象(MicroSite 和 MacroSite)是针对每个请求分别创建的。这意味着,您不必担心这些对象中的线程安全问题。

缺点

性能降低。您必须检查是否有这样的可能。所有请求都是通过 Handler 对象处理的。它使用 CommandFactory 来确定要创建哪个命令。虽然在本示例中没有性能问题,但应该仔细检查这两个类,看看是否存在任何潜在的性能问题。

其他方面的问题。该实现比 Page Controller 复杂得多。该实现的确提供了更多选择,但它的代价是复杂性和许多类。您必须权衡是否值得采用该实现。在您采用该实现并构建了框架后,可以很容易地添加新的命令和视图。不过,由于 Page Controller 是在 ASP.NET 中实现的,与在其他平台上相比,Front Controller 的实现不会同样多。

测试考虑事项。由于 Front Controller 是在 ASP.NET 中实现的,因此很难单独测试。要提高可测试性,应该将要测试的功能从依赖于 ASP.NET 的代码中分离到不依赖于 ASP.NET 的类中。然后,您不必启动 ASP.NET 运行库就可以测试这些类。

无效的 URL。因为 Front Controller 根据输入参数和应用程序的其他当前状态来决定要转到哪个视图,因此,URL 可能不会总是转到同一个页面。这样就会让用户无法保存 URL,也就无法随后再访问该页面。

返回页首返回页首

相关模式

有关详细信息,请参阅以下相关模式:

Template Method [Gamma95]BasePage 类的 PageLoadEvent 方法是 Template Method 的实现示例。

Intercepting Filter.

Page Controller.

Command [Gamma95].

Factory。此模式前面所描述的工厂将来自 Factory Method [Gamma95] Abstract Factory [Gamma95] 的元素组合在一起。

返回页首返回页首

致谢

[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

[Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.