Skip to content

9. [SimuPaie] 应用程序 – 版本 5 – ASP.NET / Web 服务


推荐阅读:参考文献 [2],《C# 2008 入门》,第 10 章“Web 服务”


9.1. 应用程序的新架构

Pam 应用程序的分层架构目前如下:

我们将按以下方式对其进行演进:

在之前的架构中,[Web]、[业务]和[DAO]层运行在同一个.NET虚拟机中,而在新架构中,[Web]层将运行在与[业务]和[DAO]层不同的虚拟机中。 例如,当 [web] 层位于机器 M1 上,而 [business] 和 [DAO] 层位于机器 M2 上时,情况便是如此。这里我们采用的是客户端/服务器架构:

  • 服务器由 [业务] 和 [DAO] 层组成。由于这是 Web 服务,因此需要 Web 服务器 #2 来运行。
  • 客户端由 [web] 层组成。运行时,它需要 Web 服务器 #1。
  • 客户端和服务器通过 TCP/IP 网络使用 HTTP/SOAP 协议进行通信。为此,必须在架构中添加两个新层:
    • [S]层,即Web服务。该Web服务接收来自远程客户端的请求,并利用[业务]层和[DAO]层来处理这些请求。构建TCP/IP服务的方法多种多样。Web服务的优势体现在两方面:
      • 它使用HTTP协议,该协议可穿透企业及政府防火墙
      • 它采用标准的 HTTP/SOAP 子协议,该协议已被众多开发平台(如 .NET、Java、PHP、Flex 等)所实现。因此,Web 服务可被 .NET、Java、PHP、Flex 等客户端“调用”(标准术语)
    • 第 [C] 层将作为远程 Web 服务的客户端。其作用是与 Web 服务 [S] 进行通信。

这种新架构可以毫不费力地从之前的架构中推导出来:

  • [业务]层和[DAO]层保持不变
  • [Web] 层略有演变,主要是为了引用 Employee Payroll 等实体,这些实体已成为客户端层 [C] 的实体。这些实体与 [业务] 或 [DAO] 层中的实体类似,但属于不同的命名空间。
  • 服务器层 [S] 是一个实现 [业务] 层 IPamMetier 接口的类。该实现仅调用 [业务] 层的相应方法。服务器层 [S] 实现的方法将“暴露”给远程客户端,客户端可调用这些方法。
  • 客户端层 [C] 将由 Visual Studio 自动生成。

新架构的原则如下:

  • [Web]层继续与[业务]层进行通信,就如同它们是本地组件一样。为了实现这一点,客户端层[C]实现了实际[业务]层的IPamMetier接口,并向[Web]层展示自己为本地[业务]层。除了前面提到的命名空间问题外,[Web]层保持不变。这就是分层工作的优势。 如果我们构建的是单层应用程序,则需要进行大规模改造。
  • 客户端层 [C] 会将 [Web] 层的请求透明地转发给远程 Web 服务 [S]。它负责处理“网络通信”的所有方面。它接收来自远程 Web 服务的响应,并将其格式化为 [Web] 层所期望的形式后返回。
  • 在服务器端,Web 服务 [S] 接收来自远程客户端的命令。它将这些命令格式化,以调用 [业务] 层中 IPamMetier 接口的方法。一旦收到来自 [业务] 层的响应,它便将其格式化,通过网络传输给客户端 [C]。[业务] 和 [DAO] 层无需进行修改。

9.2. Web 服务的 Visual Web Developer 项目

我们正在使用 Visual Web Developer 创建一个新项目:

  • 在 [1] 中,我们选择一个 C# Web 项目
  • 在[2]中,我们选择了“ASP.NET Web服务应用程序”
  • 在 [3] 中,我们为 Web 项目命名
  • 在 [4] 中,我们指定该项目的存储位置
  • 在[1]中,生成的项目。这是一个标准Web项目,具体信息如下:
    • 我们指定该项目为“Web 服务”。Web 服务不会向客户端发送 HTML 网页,而是发送 XML 格式的数据。因此,通常会生成的 [Default.aspx] 页面并未创建。
  • 在 [2] 中,生成了一个名为 [Service1.asmx] 的文件,其内容如下:

<%@ WebService Language="C#" CodeBehind="Service1.asmx.cs" Class="pam_v5_webservice.Service1" %>
  • (待续)
    • - WebService 标签表示 [Service.asmx] 是一个 Web 服务
    • - CodeBehind 属性指定了此 Web 服务的源代码位置
    • - Class 属性指定了源代码中实现该 Web 服务的类名称

默认 Web 服务的源代码 [Service.asmx.cs] 如下:


using System.Web.Services;
 
namespace pam_v5_webservice
{
  /// <summary>
  /// Service summary description1
  /// </summary>
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  [System.ComponentModel.ToolboxItem(false)]
  // To allow this Web service to be called from a script using ASP.NET AJAX, remove the comment marks from the following line. 
  // [System.Web.Script.Services.ScriptService]
  public class Service1 : System.Web.Services.WebService
  {
 
    [WebMethod]
    public string HelloWorld()
    {
      return "Hello World";
    }
  }
}
  • 第 8 行:WebService 注解,它使第 13 行的 Service1 类作为 Web 服务对外暴露。Web 服务属于某个命名空间,以防止世界上两个 Web 服务具有相同的名称。我们稍后需要更改此命名空间。
  • 第 13 行:Service1 类继承自 .NET 框架的 WebService 类。
  • 第 16 行:WebMethod 注解确保了被如此标注的方法将向远程客户端公开,从而允许它们调用该方法。
  • 第 17–20 行:HelloWorld 方法是一个演示方法。我们稍后会将其删除。它使我们能够进行初步测试,并探索 Visual Studio 工具以及有关 Web 服务的某些关键概念。
  • 在 [1] 中,我们运行 Web 服务 [Service.asmx]
  • VS Web Developer 已启动其内置 Web 服务器,并监听一个随机端口(此处为 1599)。随后向 Web 服务器请求了 URL [2]。这是 Web 服务测试页面的 URL。
  • 在 [3] 中,有一个链接可供您查看 Web 服务描述文件。该文件因其后缀(.wsdl)而被称为 WSDL(Web 服务描述语言)文件,它是一个 XML 文件,用于描述 Web 服务所公开的方法。客户端正是通过此 WSDL 文件来了解:
    • Web 服务的命名空间
    • Web 服务公开的方法列表
    • 每个方法所需的参数
    • 每个方法返回的响应
  • 在[4]中,Web服务 所公开的唯一方法。
  • 在[5]中,通过链接[3]获取的WSDL文件内容。请注意URL[6]。Web服务客户端必须知道此URL。
  • 在[7]中,通过点击链接[4]访问的页面允许您调用该Web服务的[HelloWorld]方法
  • 在[8]中,获得的结果:一个XML响应。请注意该方法的URL [9]。

通过分析前面的页面,我们可以了解 Web 服务方法的调用方式及其返回的响应类型。这使我们能够编写能够与 Web 服务通信的 HTTP 客户端。大多数现代集成开发环境(IDE)都支持自动生成此类 HTTP 客户端,从而省去了开发人员手动编写代码的麻烦。Visual Studio Express 尤其如此。

在继续进行本项目之前,我们将更改生成类时使用的默认命名空间:

当我们选择项目属性(右键单击项目 / 属性)时,可以看到 [1] 项目程序集名称和 [2] 其默认命名空间。

完成此操作后,

  • 在 [Service1.asmx.cs] 中,我们将类命名空间修改为:

using System.Web.Services;
 
namespace pam_v5
{
...
  public class Service1 : System.Web.Services.WebService
  {
...
  }
}
  • 在 [Service.asmx] 中,我们还更改了 [Service1] 类所使用的命名空间(右键单击 / 查看源代码):

<%@ WebService Language="C#" CodeBehind="Service1.asmx.cs" Class="pam_v5.Service1" %>

让我们回到应用程序的架构:

  • [S]层是Web服务。它只是将[业务]层的方法暴露给远程客户端。这就是我们目前正在构建的层。
  • [C]层是Web服务的HTTP客户端。这是IDE可以自动生成的层。
  • 如果确保 [C] 层实现了远程 [business] 层的接口,那么 [web] 层就会将 [C] 层视为本地 [business] 层。

如下所示,我们的 Web 服务将:

  • 暴露 [业务] 层的方法
  • 与[业务]层进行通信,而[业务]层则会与[DAO]层进行通信。

因此,该项目必须使用 [业务] 层和 [DAO] 层的 DLL。其演变过程如下:

  • 在 [1] 中,我们向项目添加引用
  • 在 [2] 中,我们从 [lib] 文件夹中选择常用的 DLL。我们将确保其“复制到本地”属性设置为 True。所选的 DLL 是那些支持 NHibernate 并实现 [business] 和 [DAO] 层的 DLL。

“ASP.NET Web Service”类型的 Web 应用程序可以像经典的“ASP.NET Web Site”应用程序一样拥有全局应用程序类“Global.asax”。我们已经看到了此类带来的好处:

  • 它在应用程序启动时被实例化,并始终驻留于内存中
  • 因此,它可存储所有客户端共享的只读数据。在我们的应用程序中,它将像之前那些应用程序一样,存储简化的员工列表。这样可以避免在客户端请求时,每次都从数据库中检索该列表。
  • 在 [1] 中,右键单击项目
  • 在 [2] 中,选择 [添加新项] 选项
  • 在 [3] 中,选择 [全局应用程序类]
  • 在 [4] 中,[Global.asax] 文件已添加到项目中

[Global.asax] 文件的内容如下:


<%@ Application Codebehind="Global.asax.cs" Inherits="pam_v5.Global" Language="C#" %>

[Global.asax.cs] 文件的内容如下:


using System;
 
namespace pam_v5
{
  public class Global : System.Web.HttpApplication
  {
 
    protected void Application_Start(object sender, EventArgs e)
    {
 
    }
...
  }
}

Application_Start 方法中我们应该做什么?与之前的 Web 应用程序完全一样。让我们回到应用程序架构中,将 [Global] 类放置在那里:

在上图中,

  • [Global] 类在 Web 服务启动时被实例化。只要 Web 服务处于活动状态,它就会一直驻留在内存中。
  • [Global] 类在其 [Application_Start] 方法中实例化 [business] 和 [DAO] 层
  • 为提升性能,[Global]类将简化的员工列表存储在内部字段中,并从该字段中检索员工列表。
  • Web 服务会在每次客户端请求时被实例化。处理完该请求后,它便会消失。它不会直接与 [business] 层通信,而是通过 [Global] 类进行通信。后者将实现 [business] 层的接口。

[Global] 类与之前应用程序中构建的类类似:


using System;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
 
namespace pam_v5
{
  public class Global : System.Web.HttpApplication
  {
    // --- static application data ---
    public static Employe[] Employes;
    public static IPamMetier PamMetier = null;
 
    protected void Application_Start(object sender, EventArgs e)
    {
      // instantiation layer [metier]
      PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      // retrieve the simplified employee table 
      Employes = PamMetier.GetAllIdentitesEmployes();
    }
 
    // simplified list of employees
    static public Employe[] GetAllIdentitesEmployes()
    {
      return Employes;
    }
 
    // employee salary
    static public FeuilleSalaire GetSalaire(string SS, double heuresTravaillées, int joursTravailles)
    {
      return PamMetier.GetSalaire(SS, heuresTravaillées, joursTravailles);
    }
  }
}

[Global] 类实现了 [IPamMetier] 接口,但在声明中并未明确指出这一点:


  public class Global : System.Web.HttpApplication, IPamMetier

事实上,方法 GetAllEmployeeIDs(第 24 行)和 GetSalary(第 30 行)是静态的,而 IPamMetier 接口的方法则不是。 因此,Global 类无法实现 IPamMetier 接口。此外,也无法将 GetAllEmployeeIDsGetSalary 方法声明为非静态方法。这是因为它们是通过类名而非通过类的实例来调用的。

  • 第 15 行:Application_Start 方法与之前版本中学习的 [Global] 类中的方法类似。它实例化 [business] 层(第 18 行),然后初始化(第 20 行)第 12 行中的员工数组。
  • 第 24 行:GetAllEmployeeIDs 方法仅返回第 12 行定义的员工数组。这就是将其存储在应用程序开头的优势。
  • 第 30 行:GetSalaire 方法调用 [business] 层中同名的方法。

为了实例化 [business] 层(第 18 行),[Global] 类使用了 Spring 框架。该配置由 [Web.config] 文件完成,其内容与前一个项目完全一致:它配置了 Spring 和 NHibernate,以实例化 Web 服务的 [business] 和 [DAO] 层。

让我们回到客户端/服务器应用程序的架构:

在服务器端,剩下的工作就是编写 [S] Web 服务本身。如果我们回顾应用程序架构:

我们可以看到,在服务器端,位于[业务]层之前的所有层都实现了其接口IPamMetier。这并非强制要求,但是一种合乎逻辑的做法。这种思路同样适用于客户端,即Web服务[S]的客户端[C]。因此,所有位于[Web]层与[业务]层之间的层都实现了IPamMetier接口。 因此,可以说我们又回到了三层架构:

  • 表示层 [Web] [1]
  • [业务]层 [2]
  • 数据访问层 [3]

Web 服务 [Service1.asmx.cs] 的实现可能如下:


using System.Web.Services;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
 
namespace pam_v5
{
  [WebService(Namespace = "http://st.istia.univ-angers.fr/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  [System.ComponentModel.ToolboxItem(false)]
  public class Service1 : System.Web.Services.WebService, IPamMetier
  {
 
    // list of all employee identities 
    [WebMethod]
    public Employe[] GetAllIdentitesEmployes()
    {
      return Global.GetAllIdentitesEmployes();
    }
 
    // ------- salary calculation 
    [WebMethod]
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillees, int joursTravailles)
    {
      return Global.GetSalaire(ss, heuresTravaillees, joursTravailles);
    }
  }
}
  • 第 8 行:该类被 [WebService] 属性注解,并为 Web 服务命名空间指定了一个名称
  • 第 11 行:[Service1] 类继承自 [WebService] 类,并实现了 [IPamMetier] 接口
  • 第 15 行和第 22 行:该类的每个方法都标注了 [WebMethod] 属性,以便向远程客户端公开。默认情况下,Web 服务的所有公共方法都会被公开。因此,第 15 行和第 22 行中的属性在此处是可选的。为了实现 [IPamMetier] 接口,每个方法只需调用 [Global] 类中同名的方法即可。

现在我们可以运行该 Web 服务了:

  • 在 [1] 中,项目被重新生成
  • 在 [2] 中,我们选择 Web 服务 [Service1.asmx] 并在浏览器 [3] 中显示它
  • 在 [4] 中,显示的网页。它展示了 Web 服务的方法。
  • 在 [4] 中,我们点击链接 [GetAllIdentitesEmployes],并在 [5] 中获取该方法的测试页面。
  • 在 [6] 中,该方法的 URL
  • 在 [7] 中,用于测试该方法的 [Call] 按钮。该方法无需任何参数。
  • 在 [8] 中,是 Web 服务返回的 XML 结果。在此结果中,只有 Employee 对象的 SSLastName FirstName 属性具有实际意义,因为 [GetAllEmployeeIDs] 方法仅请求这些属性。不过,该方法返回的是一个 Employee 对象数组。 从 [8] 中可以看到,数值属性 Id Version 包含在返回的 XML 流中,但具有 null 值的属性(AddressCityZipCodeAllowances)则未包含。

我们已有一个可用的 Web 服务。接下来我们将为其编写一个 C# 客户端。为此,我们需要该 Web 服务 WSDL 文件的 URI。我们可从执行 [Service.asmx] 时初始显示的页面中获取该 URI:

  • 在 [1] 中,Web 服务的 URI
  • 在 [2] 中,其 WSDL 文件的链接
  • 在 [3] 中,该链接的值

9.3. Web 服务的 NUnit 客户端的 C# 项目

我们为 Web 服务客户端创建一个 C# 项目(使用 Visual C#,而非 Visual Web Developer)。这将是一个 NUnit 测试客户端。因此,该项目将属于“类库”类型。

  • 在 [1] 中,我们创建了一个“类库”类型的 C# 项目
  • 在 [2] 中,我们为项目命名
  • 在 [3] 中,我们删除 [Class1.cs]。
  • 在 [4] 中,新建项目。
  • 在项目属性中的 [应用程序] 选项卡 [5] 上,我们设置项目命名空间。IDE 生成的每个类都将位于此命名空间中。

我们将新项目保存到自选位置:

 

完成上述步骤后,我们将生成远程 Web 服务客户端。为了理解接下来的操作,我们需要重新审视当前正在构建的客户端/服务器架构:

IDE将根据Web服务WSDL文件[S]的URI生成客户端层[C]。请注意,该文件的URI此前已记录在案。我们将从Web 开始,具体步骤如下:

  • 在 [1] 中,右键单击“引用”分支并添加服务引用
  • 在 [2] 中,输入之前记录的 Web 服务 WSDL 文件的 URL。如果服务尚未运行,请确保其已启动。
  • 在 [3] 中,通过其 WSDL 文件请求发现 Web 服务
  • 在 [4] 中,显示已发现的 Web 服务
  • 在 [5] 中,Web 服务公开的方法。
  • 在 [6] 中,选择要放置生成的客户端类和接口的命名空间。
  • 确认向导

  • 在 [1] 中,生成的客户端文件( )。双击该文件以查看其内容。
  • 在 [2] 的“对象资源管理器”中,显示了 Client.WsPam 命名空间的类和接口。这是生成的客户端的命名空间。
  • 在 [3] 中,是实现 Web 服务客户端的类。
  • 在 [4] 中,显示了客户端 [Service1SoapClient] 实现的方法。在此处,您将找到远程 Web 服务的两个方法 [5] 和 [6]。
  • 在 [2] 中,您可以查看各层的实体图:
    • [业务]:PayrollPayrollItems
    • [DAO]:EmployeeContributionsAllowances

接下来,请注意这些远程实体的表示位于客户端,且位于 PamV5Client.WsPam 命名空间中。

让我们来查看其中一个对象所暴露的方法和属性:

  • 在[1]中,我们选取本地[Employee]类
  • 在 [2] 中,我们可以看到远程 [Employee] 实体的属性,以及用于满足本地实体特定需求的私有字段。

让我们回到我们的 C# 应用程序。我们向其中添加一个 NUnit 测试类:

  • 在 [1] 中,添加了 [NUnit] 类。该 [NUnit] 类需要 NUnit 框架,因此需要引用其 DLL。这里我们假设 NUnit 框架已安装在计算机上(http://nunit.org/)。
  • 在 [2] 中,我们向项目添加引用
  • 在 [3] 的 .NET 选项卡中,该选项卡列出了计算机上已注册的 DLL,我们选择 [4],即 [nunit.framework] DLL,版本 2.4.6 或更高。

此外,我们将使用 Spring 来实例化 Web 服务 [S] 的本地客户端 [C]:

如果 DLL 已注册在计算机上,则可以与添加 NUnit 框架引用相同的方式添加 Spring DLL 的引用(http://www.springframework.net/download.html)。

我们的做法有所不同。我们将使用之前项目中的 [lib] 文件夹(其中包含 Spring 所需的 DLL),并将 Spring 引用添加到项目中:

让我们重新审视当前正在开发的客户端架构:

上图中,我们可以看到测试客户端 [1] 与一个扩展的 [业务] 层 [2] 进行交互。该层拥有与远程 [业务] 层相同的方法。因此,我们可以使用在 C# 项目 [pam-metier-dao-nhibernate] 中测试 [业务] 层时已经用过的测试类:


using NUnit.Framework;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
 
namespace Pam.Metier.Tests {
 
    [TestFixture()]
    public class NunitTestPamMetier : AssertionHelper {
 
        // the [metier] layer to test 
        private IPamMetier pamMetier;
 
        // manufacturer
        public NunitTestPamMetier() {
            // layer instantiation [dao]
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        }
 
 
        [Test]
        public void GetAllIdentitesEmployes() {
            // audit no. of employees 
            Expect(2, EqualTo(pamMetier.GetAllIdentitesEmployes().Length));
        }
 
        [Test]
        public void GetSalaire1() {
            // wage sheet calculation 
            FeuilleSalaire feuilleSalaire = pamMetier.GetSalaire("254104940426058", 150, 20);
            // checks 
            Expect(368.77, EqualTo(feuilleSalaire.ElementsSalaire.SalaireNet).Within(1E-06));
            // non-existent employee payslip 
            bool erreur = false;
            try {
                feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (PamException) {
                erreur = true;
            }
            Expect(erreur, True);
        }
 
    }
}

需要进行一些修改:

  • 在第 18 行,我们使用 Spring 框架实例化 [business] 层。这两种情况下的类并不相同。在此,本地 [business] 层是 [PamV5Client.WsPam.Service1SoapClient] 类的实例,该类由 IDE 生成。因此,在 C# 项目的 [app.config] 文件中,Spring 的配置如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 
    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>
 
    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object id="pammetier" type="PamV5Client.WsPam.Service1SoapClient, pam-v5-client-csharp-webservice"/>
        </objects>
    </spring>
 
 
    <system.serviceModel>
...
  • 上文第 16 行中,[pammetier] 对象是位于 [pam-v5-client-csharp-webservice] 程序集中的 [PamV5Client.WsPam.Service1SoapClient] 类的实例。要查找第一条信息,只需返回“对象资源管理器”中 [Service1SoapClient] 类的定义(第 9.3 节):
  • 在 [2] 中,本地 [business] 层的实现类,以及在 [1] 中其命名空间
  • 在 [3] 中,即项目属性中的程序集名称,这是配置 Spring 对象 [pammetier] 所需的第二条信息。

让我们回到 [NUnit.cs] 中用于实例化本地 [业务] 层的代码:


        // the [metier] layer to test 
        private IPamMetier pamMetier;
 
        // manufacturer
        public NunitTestPamMetier() {
            // layer instantiation [dao]
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        }
 

第 7 行:远程 [业务] 层的类型为 IPamMetier。在此处,[业务] 层的类型为 [Service1SoapClient]:


public class Service1SoapClient : System.ServiceModel.ClientBase<Service1Soap>

我们可以看到,尽管 Service1SoapClient 类暴露了同名的方法,但它并未实现 IPamMetier 接口。因此,我们必须将本地 [业务] 层的实例化代码编写如下:


        // the [metier] layer to test 
        private Service1SoapClient pamMetier;
 
        // manufacturer
        public NunitTestPamMetier() {
            // instantiation layer [metier]
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as Service1SoapClient;
}

还需要进行另一项修改:


        [Test]
        public void GetSalaire1() {
...
            try {
                feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (PamException) {
                erreur = true;
            }
            Expect(erreur, True);
        }
 

上述代码在第 6 行使用了 PamException 类型,该类型在客户端并不存在。我们将用其父类 Exception 类型来替换它。


        [Test]
        public void GetSalaire1() {
...
            try {
                 feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (Exception) {
                erreur = true;
            }
            Expect(erreur, True);
        }
 

最后,导入的命名空间不再相同:


using System;
using PamV5Client.WsPam;
using NUnit.Framework;
using Spring.Context.Support;

完成上述操作后,即可构建“类库”项目。此时将生成以下 DLL:

  • 在 [1] 中,即 C# 项目的 [bin/Release] 文件夹
  • 在 [2] 中,即该项目的 DLL。

随后由 NUnit 框架运行 NUnit 测试(测试时必须启用 MySQL 数据库 dbpam_nhibernate):

  • 在 [3] 和 [4] 中,DLL [2] 被加载到 NUnit 测试应用程序中
  • 在 [5] 中,选定测试类并执行 [6]
  • 在 [7] 中,显示测试成功的结果

现在我们已经拥有了一个可运行的 Web 服务。