65.9K
CodeProject 正在变化。 阅读更多。
Home

WF4.0 简介:使用 WF 4.0 和 WF 4.0 服务构建分布式应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.61/5 (9投票s)

2009 年 7 月 1 日

CPOL

18分钟阅读

viewsIcon

81144

downloadIcon

2342

演示如何使用 WF 4.0 和数据服务构建新设计的分布式应用程序。

引言

2006 年,微软发布了 .NET Framework 3.0,它基本上是 .NET Framework 2.0 的扩展。WCF 是当时最受欢迎的,也获得了最多的关注。这种趋势一直延续到 3.5 框架的发布。

随着 .NET Framework 4.0 的发布,微软将 WF 置于核心地位,并且在许多增强功能的支持下,WF 4.0 在构建基于 .NET 的应用程序方面的重要性不言而喻。

什么是 WF?

好吧,回答这个问题并不是本文的重点。了解 WF 是什么以及何时使用它,可以从各种在线资源或书籍中获得。例如,David Chappell 在 MSDN 上发表的一篇关于 WF 4.0 的早期优秀文章;阅读它就足以让你了解 WF 4.0 是什么以及何时使用它。你可以在这里找到它:http://msdn.microsoft.com/en-us/library/dd851337.aspx

那么本文是关于什么的?

总而言之,本文有两个主要目标:

  1. 首次介绍 WF 4.0(本文撰写时为 beta1)。
  2. 演示如何使用 WF 4.0 通过 WF 4.0 服务构建分布式应用程序。
  3. 使用 .NET 4.0 数据服务执行数据访问。

注意: 一篇关于 WF 4.0 和 WF 3.5 之间差异的精彩博文可以在这里找到:http://bloggingabout.net/blogs/pascal/archive/2009/05/20/wf-4-0-what-is-different-from-3-x.aspx

应用程序架构

该应用程序演示了一个值得展示的汽车租赁系统。下图显示了应用程序的设计。

1.JPG

场景如下:一家汽车租赁公司将其信息存储在 SQL Server 数据库中。他们需要构建一个应用程序,允许员工查询汽车价格,然后根据客户输入为客户预订汽车。为此,一个包含业务逻辑的 WF 4.0 服务通过 HTTP 托管,客户端可以向其请求两个操作:CheckPrice 和 BookCar。WF 4.0 服务——作为业务流程——不允许直接访问数据库;相反,它与 ADO.NET 数据服务通信,后者执行数据库的 CRUD 操作(稍后详述)。该系统支持哪些类型的客户端?答案是几乎任何客户端。在此示例中,我们将看到一个 .NET Windows 客户端,以及更有趣的 WF 4.0 客户端。

有没有发现任何相似之处……?

检查上述架构应该会立即触发你开发中的多层应用程序概念。事实上,上述设计不过是传统的、备受喜爱的三层设计。让我们进行匹配:

  • 数据访问层:使用 ADO.NET 数据服务代替传统的 DAL 类库和著名的 SQLHelper 类。
  • 业务层:使用 WF 4.0 服务作为业务层,而不是我们过去编写的 BAL 类库。
  • 表示层:使用 WF 4.0 控制台客户端应用程序代替 .NET 控制台应用程序。这有点棘手;在本例中,不需要用户界面,因此控制台 WF 应用程序就足够了。然而,在需要 GUI 的情况下,可以轻松使用 Windows(或 Web)客户端应用程序……毕竟,业务逻辑托管在 WF 服务中,并且可以被任何客户端像使用 XML Web 服务一样消费。

因此,在更传统的设计中,架构可能类似于下图

2.JPG

所以,新技术的出现带来了更好的设计。在本文的后续内容中,你将看到基于 WF 的新设计如何超越旧的传统设计……

源代码

源代码可以从上面的链接下载。我将在文章的进行过程中使用解决方案的组件。不可能一步一步地展示我是如何构建应用程序的,所以在进行过程中,我将引用解决方案的各个组件并解释每个组件。

必备组件

该示例基于 .NET 4.0 和 VS 2010 Beta1 版本构建。你可以在这里下载它们:http://www.microsoft.com/downloads/details.aspx?familyid=3296BB4F-D8BA-4CFD-AA95-A424C5913F6B&displaylang=en。你还需要 SQL Server 2000/2005。开始吧……

第一部分:数据库

与任何以数据为中心的方法一样,应用程序设计通常从数据库开始,然后向上层发展。我们将遵循这里的概念。幸运的是,对于我们的例子,数据库很简单,只有一个名为 Cars 的表,如下所示。

3.JPG

CarId 列包含一些著名制造商的名称,如 Honda 和 Mercedes。Day_UnitPrice 列包含每日租金费用,最后 Quantity 列包含可供租赁的汽车数量。

在附件文件中,你会找到一个名为 CarStore.bak 的文件;请将其还原到你的 SQL Server。

第二部分:ADO.NET 数据服务

数据服务遵循表示状态转移 (REST) 设计范例。这种设计完全依赖于纯 HTTP 动词:POST、GET、PUT 和 DELETE,分别用于执行创建、读取、更新和删除 (CRUD) 操作。使用 REST,你可以享受简洁和便捷,当你不必处理 SOAP 与 XML Web 服务或 WCF 的复杂性(尽管有时这是绝对必须的)时,它最适合。当你不需要 SOAP 加密、WS-路由、WS-寻址以及任何其他 WS-* 标准时,数据服务可以成为执行 CRUD 操作的最佳选择。

网上有很多关于 REST 与 SOAP 主题的精彩讨论,我鼓励你阅读并获得更多关于何时使用每种方法的见解。

创建 ADO.NET 数据服务时要做的第一件事就是公开数据源。我使用了实体框架来实现这一点。所以,请按照以下步骤设置数据服务项目:

  1. 创建一个空的 VS 2010 解决方案,并将其命名为“CarRentalDemo”。
  2. 在附件文件中,你会找到一个名为“CarRentalDataService”的文件夹。将此文件复制到你的 intepub/wwwroot 文件夹中,并在 IIS 中创建其应用程序。
  3. 将“CarRentalDataService”项目添加为现有网站到“CarRentalDemo”解决方案。

现在,让我们看看是什么构成了数据服务项目。在 VS 中,你会看到以下文件:

  1. CarRental.edmx:这将创建一个 CarStore 数据库的对象表示。在通过数据服务以 REST 风格公开数据库之前,我们需要拥有该数据库的对象模型。这个对象模型最好使用 ADO.NET 实体框架来表示。
  2. CarRentalService.svcCarRentalService.cs:现在对象模型已准备好,我们需要创建数据服务本身。CarRentalService.cs 用于指示我们要公开的实体模型是什么,而 CarRentalService.svc 是指向 CarRentalService 类的物理数据服务。

这就是构建数据服务所需的所有内容。现在,从 Visual Studio 中,你可以右键单击 CarRentalService.svc 并选择“浏览”,你将能够以 REST 风格查询数据源。例如,要选择 ID 为“Honda”的汽车,请使用以下 URL:https:///CarRentalDataService/CarRentalService.svc/Cars('Honda')

使用浏览器只足以进行浏览;但是,为了使数据服务在我们的示例中可用,我们将不得不使用 .NET 代码执行 CRUD 操作。这将在下一步中展示。

第三部分:自定义活动

按照架构图,我们现在应该看到 WF 4.0 服务。在此之前,让我们讨论自定义活动。活动是 WF 的构建块。你从工具箱拖到 WF 设计器中的每个形状都是一个活动。自定义活动允许开发人员使用为手头问题编写的新活动来扩展现有活动;这是一种特定领域语言 (DSL)。

特别是 WF 4.0 鼓励使用自定义活动。在 4.0 之前,WF 有一个 Code Activity 形状,允许开发人员直接在 WF 进程中编写代码。在 WF 4.0 中,Code Activity 形状已消失,现在开发人员必须创建自定义活动来构建他们的组件。

在我们的示例中,我们需要在一系列自定义活动中同时使用 WF 4.0 服务和 WF 4.0 客户端。在附件文件中,你会找到一个名为“CustomActivities”的项目;这是一个类库,其中编译了所有活动。将此项目添加到你的“CarRentalDemo”解决方案中。让我们检查它的内容:

GetInput.cs:此活动通过控制台收集用户输入,并将其分配给输出参数。参数是 WF 4.0 中数据进出进程的方式。有输入参数和输出参数。以下是活动的代码:

public class GetInput : CodeActivity
{
    OutArgument<string> data; 
    public OutArgument<string> Data 
    {
        get { return data; }
        set { data = value; } 
    }

    protected override void Execute(CodeActivityContext context)
    {
        string input = Console.ReadLine();
        context.SetValue(data, input);
    }
}

首先,请注意活动如何继承自 CodeActivity 类。然后,我们定义一种特殊的数据类型 OutArgument,指示 WF 需要将此数据作为自定义活动的输出暴露。然后,我们实现所需的单个方法,即“Execute”方法。在这里,我们编写所需代码——即从控制台获取数据——然后使用活动上下文通过 CodeActivityContext 类将此数据分配给 OutArgument

CheckPrice.cs:此活动使用数据服务并请求特定汽车的价格。让我们看看这个活动的 कोड:

public class CheckPrice : CodeActivity
{
    InArgument<string> carId;
    public InArgument<string> CarId
    {
        get { return carId; }
        set { carId = value; }
    }

    OutArgument<decimal> carPrice;
    public OutArgument<decimal> CarPrice
    {
        get { return carPrice; }
        set { carPrice = value; }
    }

    protected override void Execute(CodeActivityContext context)
    {
        string carId = CarId.Get(context);

        String urlstr = "http://mhalabi/CarRentalDataService" + 
                        "/CarRentalService.svc";
        CarRentalReference.CarStoreEntities proxy = 
          new CarRentalReference.CarStoreEntities(new Uri(urlstr));

        var query = (from c in proxy.Cars
                     where c.CarId == carId
                     select c).First();
        decimal? price = query.Day_UnitPrice;
        context.SetValue(CarPrice, price);
    }
}

对于此活动,我们希望将 CarId 作为输入,并将 CarPrice 作为输出。为此,我们定义了一个输入参数 (CarId) 和一个输出参数 (CarPrice)。输入参数将从 WF 进程本身传递到活动;稍后将看到这一点。为了获取输入参数的值,我们再次使用 CodeActivtyContext 类。

现在我们有了 CarId,我们需要使用它来查询数据服务并获取该特定汽车的价格。数据服务——正如你之前所见——在物理上是一个 SVC 文件(类似于 WCF 的 SVC 文件),因此在查询它之前,我们需要添加一个服务引用。这在项目中完成,并且代码使用生成的代理。在代码中,我们使用 LINQ 来查询服务并获取我们想要的汽车的价格。这里的 LINQ 可以被认为是 LINQ to URI……最后,我们设置输出参数 CarPrice 的值。

BookCar.cs:此活动也使用数据服务来预订特定汽车。由于与上一个活动相似,我在这里不展示完整代码,但总的来说,此活动定义了一个输入参数 CarId,然后将该参数传递给 Execute 方法,该方法使用数据服务来预订相应的汽车。

第四部分:WF 4.0 服务

WF 服务就是将 WF 进程通过 Web 服务——更具体地说,是 WCF——公开。有了 WF 服务,你可以继续利用 WF 的强大功能来构建业务流程,并具有将这些流程托管在 Web 服务上并使其可供消费的额外能力。这绝对是一个能极大地增强分布式应用程序设计的强大功能,就像本文的目的一样。

在附件代码中,你会找到一个名为“CarRentalService”的项目。这是一个控制台工作流应用程序类型的项目。让我们开始解析它的组件:

Program.cs:与任何控制台应用程序一样,需要一个入口点,WF 控制台应用程序也不例外。这个应用程序的特别之处——作为一个 WF 服务而不是一个普通的 WF 程序——是需要将其托管为 WCF 服务;因此,下面的代码将 WF 程序作为普通 WCF 服务托管在 HTTP 上。

class Program
{
    static void Main(string[] args)
    {
        string baseAddress = "https://:8089/CarRentalService";

        using (WorkflowServiceHost host = 
               new WorkflowServiceHost(typeof(RentCar), new Uri(baseAddress)))
        {
            host.Description.Behaviors.Add(new 
              ServiceMetadataBehavior() { HttpGetEnabled = true });
            host.AddDefaultEndpoints();

            host.Open();
            Console.WriteLine("Car rental service listening at: " + 
                              baseAddress);
            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();
            host.Close();
        }

    }
}

WorkflowServiceHost (System.ServiceMode.Activities.dll) 类负责托管 WF 4.0 服务,就像众所周知的 ServiceHost (System.ServiceMode.dll) 类负责 WCF 托管一样。在我们的例子中,WF 服务将托管在以下 HTTP URI 上:https://:8089/CarRentalService

现在,这个 WF 服务可以像任何其他 WCF 服务一样被引用和消费。

有了托管,现在让我们首次了解新的 WF 4.0 设计器,并看看 CarRentalService 实际上是如何构建的。

WF 4.0 最酷的新特性之一是 XAML 驱动的设计器。RentCar.xaml 是表示物理流程的文件。首先看到的是变量部分。

4.JPG

变量是 WF 4.0 中的另一个新特性。它们用于在程序生命周期内存储值。在我们的示例中,我们定义了五个变量:

  1. RequestInfo:类型为 CarRentalDataContract 的变量。CarRentalDataContract 是同一项目中定义的类,并保存用于消息交换的 WCF DataContract。它是一个简单的契约,保存正在处理的 CarId 和想要租车的 UserId 的值。
  2. CustomerId:类型为 string 的变量。
  3. CarId:类型为 string 的另一个变量。
  4. UnitPrice:类型为 decimal 的变量。
  5. ContentHandle:这是一个特殊变量,需要稍作解释。首先,正如你所见,它的类型为 CorrelationHandle,它代表一种非常常见的方法,称为关联。关联是将一条或多条消息与单个流程实例相关联的技术。为了更好地理解这一点,让我们以我们的例子为例:在我们的流程中,客户首先提交她想要租用的汽车的 CarId,然后是她的 UserId。流程会检查汽车的价格并显示给客户。客户会花时间思考,然后提交一个是/否的答案;这里的关键是,会有许多相同的 WF 进程实例为许多用户运行。因此,挑战在于将特定客户的答案“路由”到正确的 WF 进程实例。例如,我们不希望客户“A”的答案被路由到为客户“B”创建的 WF 实例。这种类型的设计称为关联,并且对于 BizTalk 开发人员来说将非常熟悉。请注意,关联对于同步消息(即请求-响应)不是必需的,因为响应会从同一请求通道返回。稍后将更清楚地说明如何使用关联……

因此,现在变量已定义,让我们看看我们的 WF 服务的第一个部分:

5.JPG

为了构建上述部分,我从工具箱的“Messaging”选项卡拖放了“ReceiveAndSendReply”活动。在它们之间,我从“Procedural”选项卡拖放了“Assign”活动,最后使用了“CustomActivitiesComponents”选项卡中的“CheckPrice”自定义活动。一旦你引用了“CustomActivities”项目并构建了解决方案,你将在工具箱中看到“CustomActivitiesComponents”选项卡。

“Receive Check Price”接收检查汽车价格的请求。操作名称设置为“CheckPrice”;这将是客户端要使用的服务操作。值被设置为变量“RequestInfo”,它是服务操作“CheckPrice”的输入参数。“Correlated with”属性设置为变量“ContentHandle”。让我们看看它是如何配置的:

6.JPG

这里,关联与 CustomerId 变量相关联,该变量被设置为 Data Contract 中 CustomerId 的 XPath 值。这意味着每当客户请求检查某种汽车的价格时,都会初始化一个新的关联,并与该客户的 CustomerId 相关联。正如我们稍后在 WF 服务的第二部分将看到的,当客户请求预订汽车时,会跟进此关联。稍后将详细介绍……

接下来,“Assign”形状将 CarId 变量分配给 Data Contract 中的相应 CarId。然后,我们调用自定义活动“CheckPrice”,将变量“CarId”作为输入参数传递给它,同时期望返回输出参数“CarPrice”,并将返回值与之关联到变量“UnitPrice”。

现在,有了价格,我们最后通过“ReceiveAndSendReply”活动的“Send Price”部分将结果返回给客户端。

WF 服务的第二部分如下图所示:

7.JPG

这部分接收预订汽车的请求,使用“BookCar”自定义活动进行预订,然后将布尔确认返回给客户端。需要注意的重要一点是“Correlates with”属性:此属性配置为跟进我们在上一部分中设置的关联;因此,现在每个预订汽车的请求都将正确地与 WF 流程的正确实例相关联。

注意: 如果同一客户发出多个订单,此设计将导致问题,因为对于同一客户,我们将有多个可能的 WF 实例进行关联(对于同一个 CustomerId)。一个更现实的设计是根据 OrderId 或 GUID 进行关联;但是,为了演示起见,本文保持了简单。

第五部分:Windows 客户端

现在 WF 服务已设置完毕,我们可以像任何其他 WCF 服务一样消费它。在附件代码中,有一个名为“TestClient”的项目;将此项目添加到你的解决方案中。下面的代码显示了如何消费 WF 服务:

CarRentalServiceReference.CarRentalDataContract contract = 
   new TestClient.CarRentalServiceReference.CarRentalDataContract();
contract.CarId = "Mercedes";
contract.CustomerId = "012";
CarRentalServiceReference.CarRentalServiceContractClient proxy = 
  new TestClient.CarRentalServiceReference.CarRentalServiceContractClient();
decimal? test = proxy.CheckPrice(contract);

bool? test1 = proxy.BookCar("012");

CarRentalServiceReference 是一个普通的 WCF 服务服务引用。回想一下,我们将 WF 服务托管在以下 URI 上:https://:8089/CarRentalService

我们只是添加了对该 URI 的服务引用(请注意,在添加服务引用之前必须运行服务;你需要有服务的宿主正在运行)。请注意,在代理中,我们暴露了两个服务操作:CheckPrice 和 BookCar;这些是我们为 WF 服务中的两个接收形状配置的操作名称……

第六部分:WF 客户端

附件代码中的“CarRentalWF”项目说明了如何使用 WF 控制台应用程序来消费 WF 服务。下面是 WF 流程的分步描述:

  1. 将 CarRentalWF 项目添加到你的解决方案中。
  2. 向 CarRentalService 项目添加服务引用(首先,运行 CarRentalService 项目以运行宿主)。构建解决方案,你将在工具箱中获得 CarRentalWFComponents 选项卡。
  3. “WriteLine”活动用于向用户显示文本,要求他们输入汽车 ID。
  4. 自定义活动“GetInput”读取输入并将其存储在变量“CarId”中。
  5. “WriteLine”活动用于显示文本,询问用户 ID。
  6. 自定义活动“GetInput”读取输入并将其存储在变量“UserId”中。
  7. “Assign”活动初始化类型为 CarRentalDataContract 的变量。
  8. 使用“Assign”活动将数据契约实例的 CarId 属性设置为 CarId 变量。
  9. 使用“Assign”活动将数据契约实例的 CustomerId 属性设置为 UserId 变量。
  10. 然后使用 CarRentalWFComponents 中的“CheckPrice”活动,并将数据契约作为参数传递。这里的 CheckPrice 活动代表一个服务操作;就像我们在 Windows 客户端中显式调用的那个一样。返回值存储在变量“Price”中。
  11. “WriteLine”活动询问用户是否要根据返回的汽车价格继续。
  12. 自定义活动“GetInput”收集用户的答案并将其存储在变量“IsContinue”中。
  13. 然后使用“If”活动,如下所示:如果用户的答案是“yes”,则预订汽车,否则流程结束。让我们看看该活动是如何配置的:

双击“If”活动,它会展开如下:

8.JPG

条件部分测试变量“IsContinue”的值。如果值为“yes”,则执行“Then”部分,否则执行“Else”部分,后者仅向用户显示一条消息。现在,让我们看看“Then”部分是如何配置的;双击“Then”形状,你将看到以下内容:

9.JPG

在这里,我们向服务操作“BookCar”发送一个请求,最后显示一条消息。

运行示例

运行 CarRentalService 项目。现在,WF 服务已运行,并准备好接受请求。

接下来,运行 CarRentalWF 项目。现在 WF 客户端控制台会提示你输入想要租用的汽车 ID。例如,输入“Honda”,然后会直接提示你输入用户 ID;例如,输入 10。

此时,CarRentalWF 实例将消耗 WF 服务“CarRentalService”的 CheckPrice 操作,该操作将通过自定义活动“CheckPrice”查询数据服务,并显示所选汽车的价格。请记住,此时 WF 服务中会初始化一个新的关联。用户现在会被提示是否继续。输入“yes”。CarRentalWF 实例将消耗 WF 服务 CarRentalService 的 BookCar 服务操作,该操作将通过自定义活动“BookCar”消耗数据服务。由于我们设置了关联,预订汽车的命令将正确路由到相应的流程实例……以下是最终控制台输出的屏幕截图:

10.JPG

WF4.0 简介:使用 WF 4.0 和 WF 4.0 服务构建分布式应用程序 - CodeProject - 代码之家
© . All rights reserved.