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

使用 WCF 利用 ASP.NET 基础结构和 BasicHttpBinding 实现有状态 Web 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (20投票s)

2010 年 7 月 20 日

CPOL

5分钟阅读

viewsIcon

78367

downloadIcon

960

本文将介绍如何通过利用 ASP.NET 基础结构和 BasicHttpBinding,在 WCF Web 服务中管理状态。

引言

本文将通过一个简化的购物车示例,介绍如何使用 BasicHttpBinding,利用 ASP.NET 基础结构(即 ASP.NET HTTP 管道)开发有状态的 WCF 服务。

背景

期望的背景知识包括 C#、ASP.NET 和 WCF。

Using the Code

在此示例中,我们将采用一种与 SOA 实现非常相似的、面向契约优先的方法。

  1. 创建一个名为 StateManagementWCF 的空白解决方案。
  2. 添加一个名为 OrderServiceContract 的类库项目。
  3. Class1.cs 重命名为 IOrderService.cs,并在其中插入以下代码
  4. 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 属性来支持会话。

  5. 现在添加另一个 WCF 服务应用程序类型的项目,并命名为 UsageService。添加对 System.ServiceModel 程序集的引用。将服务重命名为 OrderService.svc
  6. OrderService.svc.cs 文件中包含以下代码
  7. 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 类实现了 IOrderServiceIDisposable 接口。该类上应用了以下特性

    [AspNetCompatibilityRequirements(RequirementsMode = 
            AspNetCompatibilityRequirementsMode.Allowed)]

    这使得 WCF 服务能够利用现有的 ASP.NET HTTP 管道,从而获得使用 HttpContext.Current.Session 对象来存储(记住)有状态数据的“许可”。在 StartPurchase 操作期间,我们初始化会话变量中的数据,从而建立会话。在其他所有操作中,我们通过检查存储在会话变量中的(因此可以在多次调用之间记住的)TransactionStarted 布尔变量来检查会话是否已开始。

  8. 现在我们将修改 web.config。修改后的 web.config 如下所示
  9. <?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"/>
       ...
       ...

    在此,我们启用了 serviceHostingEnvironmentaspNetCompatibilityEnabled 属性。因此,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 对每个会话都是唯一的,并且对所有客户端都不同。

  10. 接下来,我们在 IIS 中创建一个名为 UsageService 的虚拟目录,并通过浏览 OrderService.svc 来测试服务。
  11. 接下来,我们向解决方案添加一个名为 ShoppingClient 的 Windows 窗体应用程序项目,并为其添加对 System.ServiceModel 的引用。
  12. 现在,我们为 ShoppingClient 添加对以下终结点的服务引用:https:///UsageService/OrderService.svc
  13. 然后,我们设计窗体的 UI,最后在 ShoppingForm.cs 中添加以下代码
  14. 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

  15. 修改后的 App.config 文件如下所示
  16. <?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 日。
© . All rights reserved.