使用 WCF 利用 ASP.NET 基础结构和 BasicHttpBinding 实现有状态 Web 服务
本文将介绍如何通过利用 ASP.NET 基础结构和 BasicHttpBinding,在 WCF Web 服务中管理状态。
引言
本文将通过一个简化的购物车示例,介绍如何使用 BasicHttpBinding,利用 ASP.NET 基础结构(即 ASP.NET HTTP 管道)开发有状态的 WCF 服务。
背景
期望的背景知识包括 C#、ASP.NET 和 WCF。
Using the Code
在此示例中,我们将采用一种与 SOA 实现非常相似的、面向契约优先的方法。
- 创建一个名为 StateManagementWCF 的空白解决方案。
- 添加一个名为 OrderServiceContract 的类库项目。
- 将 Class1.cs 重命名为 IOrderService.cs,并在其中插入以下代码
- 现在添加另一个 WCF 服务应用程序类型的项目,并命名为 UsageService。添加对 System.ServiceModel 程序集的引用。将服务重命名为 OrderService.svc。
- 在 OrderService.svc.cs 文件中包含以下代码
- 现在我们将修改 web.config。修改后的 web.config 如下所示
- 接下来,我们在 IIS 中创建一个名为 UsageService 的虚拟目录,并通过浏览 OrderService.svc 来测试服务。
- 接下来,我们向解决方案添加一个名为 ShoppingClient 的 Windows 窗体应用程序项目,并为其添加对 System.ServiceModel 的引用。
- 现在,我们为 ShoppingClient 添加对以下终结点的服务引用:https:///UsageService/OrderService.svc。
- 然后,我们设计窗体的 UI,最后在 ShoppingForm.cs 中添加以下代码
- 修改后的 App.config 文件如下所示
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Text;
namespace OrderServiceContract
{
[ServiceContract(Namespace = "OrderServiceContract",
SessionMode = SessionMode.Allowed)]
public interface IOrderService
{
[OperationContract]
void StartPurchase();
[OperationContract]
string PlaceOrder(Item item);
[OperationContract]
string MakePayment(decimal amount);
[OperationContract]
string ShipOrder(string address);
[OperationContract]
void EndPurchase();
// TODO: Add your service operations here
}
// Use a data contract as illustrated in the sample
// below to add composite types to service operations.
[DataContract]
public class Item
{
[DataMember]
public string ItemName { get; set; }
[DataMember]
public decimal Price { get; set; }
}
}
在此,我们首先声明并定义一个服务契约,并将 SessionMode
设置为 Allowed,然后定义复合对象项的数据契约。通过 StartPurchase
操作,买家可以开始购买会话。通过 PlaceOrder
操作,买家可以为某个商品下单。该操作在会话中可以被调用多次,以购买多个商品。在服务实例本身,将计算并记住已购商品的总待付金额。然后,买家可以通过调用 MakePayment
操作进行支付。每次调用时,服务实例都会记住累积的支付金额。最后,当买家调用 ShipOrder
时,服务实例会检查是否已全额支付。最终,通过调用 EndPurchase
操作终止会话。该模式通过 SessionMode
属性来支持会话。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;
namespace UsageService
{
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class OrderService : OrderServiceContract.IOrderService, IDisposable
{
private decimal TotalAmount { get; set; }
private decimal PaymentReceived { get; set; }
private bool TransactionStarted {get; set;}
public OrderService()
{
TotalAmount = 0;
PaymentReceived = 0;
TransactionStarted = false;
}
public void StartPurchase()
{
HttpContext.Current.Session["TransactionStarted"] = true;
HttpContext.Current.Session["TotalAmount"] = 0;
HttpContext.Current.Session["PaymentReceived"] = 0;
}
public string PlaceOrder(OrderServiceContract.Item item)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["TotalAmount"] =
Convert.ToDecimal(
HttpContext.Current.Session["TotalAmount"]) + item.Price;
return "Order placed for item " + item.ItemName +
" and total outstanding amount is $" +
HttpContext.Current.Session["TotalAmount"].ToString();
}
return "Shopping session not yet started";
}
public string MakePayment(decimal amount)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["PaymentReceived"] =
Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]) + amount;
return "Payment made of amount USD " +
HttpContext.Current.Session["PaymentReceived"].ToString() +
" and amount remaining to be paid is $" +
((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) -
(Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]))).ToString();
}
return "Shopping session not yet started";
}
public string ShipOrder(string address)
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
if ((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) <=
(Convert.ToDecimal(HttpContext.Current.Session["PaymentReceived"])))
{
return "Ordered items would be reaching" +
" at your doorstep soon. Thanks";
}
return "Please pay the full amount in advance in order to enable " +
"us ship your items, the outstanding amount is $" +
((Convert.ToDecimal(HttpContext.Current.Session["TotalAmount"])) -
(Convert.ToDecimal(
HttpContext.Current.Session["PaymentReceived"]))).ToString();
}
return "Shopping session not yet started";
}
public void EndPurchase()
{
if (Convert.ToBoolean(HttpContext.Current.Session["TransactionStarted"]))
{
HttpContext.Current.Session["TransactionStarted"] = false;
}
}
public void Dispose()
{
}
}
}
在此,OrderService
类实现了 IOrderService
和 IDisposable
接口。该类上应用了以下特性
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
这使得 WCF 服务能够利用现有的 ASP.NET HTTP 管道,从而获得使用 HttpContext.Current.Session
对象来存储(记住)有状态数据的“许可”。在 StartPurchase
操作期间,我们初始化会话变量中的数据,从而建立会话。在其他所有操作中,我们通过检查存储在会话变量中的(因此可以在多次调用之间记住的)TransactionStarted
布尔变量来检查会话是否已开始。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<compilation debug="true"/>
<sessionState cookieless="false" mode="InProc"/>
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
<services>
<service name="UsageService.OrderService"
behaviorConfiguration="UsageService.OrderServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="https:///UsageService" />
</baseAddresses>
</host>
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="OrderBinding"
contract="OrderServiceContract.IOrderService" />
<endpoint address="mex" binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="UsageService.OrderServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<binding name="OrderBinding" allowCookies="true">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
<directoryBrowse enabled="true" />
</system.webServer>
</configuration>
在此,我们首先将 sessionState
元素的 cookieless
属性设置为 false
,从而支持会话 cookie,并指定状态的维护方式。为了简化起见,我们暂时选择 InProc
(内存中的会话数据存储),否则我们将偏离对 ASP.NET 中状态管理技术的实际讨论。
<system.web>
<compilation debug="true"/>
<sessionState cookieless="false" mode="InProc"/>
</system.web>
接下来,我们将 ASP.NET 兼容性设置如下
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
...
...
在此,我们启用了 serviceHostingEnvironment
的 aspNetCompatibilityEnabled
属性。因此,IIS 托管该服务,IIS 以及 ASP.NET 集成模块和处理程序提供托管环境。接下来,我们可以看到我们使用了 basicHttpBinding
,它实际上模仿了早期的 ASP.NET Web 服务。现在,在绑定配置中,我们允许 cookie
<bindings>
<basicHttpBinding>
<binding name="OrderBinding" allowCookies="true">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
这样做的原因是,当客户端通过代理首次调用服务时,会创建一个服务实例,开始一个会话,并将一个会话 cookie 返回给客户端,客户端必须在每次后续的会话服务调用中将此 cookie 提交给服务器。会话 cookie 中存储了会话 ID,当服务收到每次后续服务调用中的会话 ID 时,它就知道是哪个客户端在调用它,以及该特定客户端会话的状态数据是什么,因为可能有很多客户端同时调用这些服务操作。会话 ID 对每个会话都是唯一的,并且对所有客户端都不同。
public partial class ShoppingForm : Form
{
private List<OrderServiceReference.Item> itemList = null;
private OrderServiceReference.OrderServiceClient clientService = null;
public ShoppingForm()
{
InitializeComponent();
itemList = new List<OrderServiceReference.Item>();
}
private void AddItemsToList()
{
OrderServiceReference.Item itm = new OrderServiceReference.Item()
{ ItemName = "Bag", Price = (decimal)10.70 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Boot", Price = (decimal)11.30 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Basket", Price = (decimal)10.00 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Box", Price = (decimal)20.07 };
itemList.Add(itm);
itm = new OrderServiceReference.Item()
{ ItemName = "Bat", Price = (decimal)1.93 };
itemList.Add(itm);
}
private void ShoppingForm_Load(object sender, EventArgs e)
{
try
{
clientService = new OrderServiceReference.OrderServiceClient();
clientService.StartPurchase();
this.AddItemsToList();
}
catch (Exception ex)
{
txtMessage.Clear();
txtMessage.Text = ex.Message + "\n" + ex.Source + "\n" +
ex.StackTrace + "\n" + ex.TargetSite;
}
}
private void btnExit_Click(object sender, EventArgs e)
{
Application.Exit();
}
private void btnMakePayment_Click(object sender, EventArgs e)
{
txtMessage.Clear();
if (txtAmount.Text == String.Empty)
{
MessageBox.Show("Please enter amount first");
return;
}
txtMessage.Text = clientService.MakePayment(
Convert.ToDecimal(txtAmount.Text.Trim()));
txtAmount.Clear();
}
private void btnPurchaseBag_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[0]);
}
private void btnPurchaseBoot_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[1]);
}
private void btnPurchaseBasket_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[2]);
}
private void btnPurchaseBox_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[3]);
}
private void btnPurchaseBat_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.PlaceOrder(itemList[4]);
}
private void btnShipOrder_Click(object sender, EventArgs e)
{
txtMessage.Clear();
txtMessage.Text = clientService.ShipOrder("DF-I, B-2/4, " +
"PURBA ABASAN, 1582/1 RAJDANGA MAIN ROAD, KOLKATA - 700107, INDIA");
}
}
在此,我们维护一个已订购商品的列表,并在一个名为 AddItemsToList()
的方法中填充该列表,以保持简单。现在,在 Form_Load
期间,我们调用 StartPurchasing()
操作以与服务开始会话。我们调用 PlaeOrder(item)
方法进行下单,调用 MakePayment(amount)
方法进行支付,最后调用 ShipOrder(address)
方法请求服务发货。我们看到在客户端代码中我们没有做任何关于状态管理的事情,因为诀窍在于 app.config。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IOrderService"
closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00"
sendTimeout="00:01:00"
allowCookies="true" bypassProxyOnLocal="false"
hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288"
maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8"
transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None"
proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName"
algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://kummu-pc/UsageService/OrderService.svc"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IOrderService"
contract="OrderServiceReference.IOrderService"
name="BasicHttpBinding_IOrderService" />
</client>
</system.serviceModel>
</configuration>
在此,我们在绑定中将 allowCookies
设置为 true
。代理会自动处理服务返回的会话 cookie,并在之后每次进行服务调用时将该会话 cookie 提交给服务。所有这些底层工作都是对开发人员透明的。
关注点
现在,为了提高应用程序的可伸缩性,应在 web.config 中将会话状态存储在 SQL Server 中,方法是将 mode
设置为 "SqlServer"
,而不是 mode = "InProc"
。自动创建的数据库是 ASPNETDB。当 mode="SqlServer"
时,还需要指定 connectionString
属性。通常,为了实现高可用性,SQL Server 会配置为活动-被动集群,因此,由于任何故障都不会丢失会话数据。此外,还可以通过 HTTPS 而不是 HTTP 来实现传输层安全性。在这种情况下,可以使用 makecert 工具构建证书并将其放入证书存储中,然后将公钥分发给客户端。在消息级别,可以进行编码以实现消息级别的安全性。此外,最后但同样重要的是,可以为 basicHttpBinding
配置凭据,从而利用 ASP.NET HTTP 管道处理功能的 Membership API 和 Roles API。
历史
- 发布于 2010 年 7 月 20 日。