在《基於IIS的WCF服務寄宿(Hosting)實現揭秘》中,我們談到在采用基於IIS(或者說基於ASP.NET)的WCF服務寄宿中,具有兩種截然不同的運行模式:ASP.NET並行(Side by Side)模式和ASP.NET兼容模式。對於前者,WCF通過HttpModule實現了服務的寄宿,而對於後者,WCF的服務寄宿通過一個HttpHandler實現。只有在ASP.NET兼容模式下,我們熟悉的一些ASP.NET機制才能被我們使用,比如通過HttpContext的請求下下文;基於文件或者Url的授權;HttpModule擴展;身份模擬(Impersonation)等。
由於在ASP.NET兼容模式下,ASP.NET采用與.aspx Page完全一樣的方式處理基於.svc的請求,換言之,我們就可以借助當前HttpContext的SessionState維護會話狀態,進而創建一個支持會話的WCF Service。接下來,我們就通過一個簡單的例子,一步步地創建這樣的會話服務。本案例采用如圖1所示的3層結構。
圖1 ASP.NET兼容模式案例應用結構
步驟一、定義服務契約:ICalculator
案例依然沿用計算服務的例子,不過通過原來直接與傳入操作數並得到運算結果的方式不同,為了體現會話狀態的存在,我們將本案例的WCF服務定義成“累積計算服務”:保留上一次運算的結果,並將其作為後續運算的操作數。為此,定義了如下一個接口作為服務契約:前面4個操作代表基本的加、減、乘、除運算,計算結果通過GetResult方法獲得。
1: using System.ServiceModel;
2: namespace Artech.AspCompatibleServices.Contracts
3: {
4: [ServiceContract]
5: public interface ICalculator
6: {
7: [OperationContract]
8: void Add(double x);
9: [OperationContract]
10: void Subtract(double x);
11: [OperationContract]
12: void Multiply(double x);
13: [OperationContract]
14: void Divide(double x);
15: [OperationContract]
16: double GetResult();
17: }
18: }
步驟二、實現服務:CalculatorService
服務的實現和.svc都定義在一個ASP.NET Web站點項目中。對於定義在 CalculatorService中的每次運算,先通過HttpContext從SessionState中取出上一次運算的結果,完成運算後再將新的運算結果保存到SessionState中。通過在CalculatorService上應用AspNetCompatibilityRequirementsAttribute實現對ASP.NET兼容模式的支持。
1: using System.ServiceModel.Activation;
2: using System.Web;
3: using Artech.AspCompatibleServices.Contracts;
4: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
5: public class CalculatorService : ICalculator
6: {
7: public void Add(double x)
8: {
9: HttpContext.Current.Session["__Result"] = GetResult() + x;
10: }
11: public void Subtract(double x)
12: {
13: HttpContext.Current.Session["__Result"] = GetResult() - x;
14: }
15: public void Multiply(double x)
16: {
17: HttpContext.Current.Session["__Result"] = GetResult() * x;
18: }
19: public void Divide(double x)
20: {
21: HttpContext.Current.Session["__Result"] = GetResult() / x;
22: }
23: public double GetResult()
24: {
25: if (HttpContext.Current.Session["__Result"] == null)
26: {
27: HttpContext.Current.Session["__Result"] = 0.0;
28: }
29: return (double)HttpContext.Current.Session["__Result"];
30: }
31: }
下面是CalculatorService對應的.svc的定義和Web.config。為了簡潔,在<@ServiceHost%>指令中,僅僅設置一個必需屬性Service。對於ASP.NET兼容模式的支持,配置<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>必不可少。
1: <?xml version="1.0"?>
2: <configuration>
3: <system.serviceModel>
4: <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
5: <services>
6: <service name="CalculatorService">
7: <endpoint binding="wsHttpBinding" contract="Artech.AspCompatibleServices.Contracts.ICalculator" />
8: </service>
9: </services>
10: </system.serviceModel>
11: </configuration>
步驟三、創建客戶端:Client
CalculatorService的客戶端應用通過一個Console應用程序模擬,其服務調用方式並無特別之處,下面是相關的代碼和配置。
1: using System;
2: using System.ServiceModel;
3: using Artech.AspCompatibleServices.Contracts;
4: namespace Artech.AspCompatibleServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("CalculatorService"))
11: {
12: ICalculator proxy = channelFactory.CreateChannel();
13: Console.WriteLine("初始值為: {0}", proxy.GetResult()); proxy.Add(1);
14: Console.WriteLine("Add(3)", proxy.GetResult());
15: Console.WriteLine("運算結果為: {0}", proxy.GetResult()); proxy.Multiply(10);
16: Console.WriteLine("Multiply(10)", proxy.GetResult()); Console.WriteLine("運算結果為: {0}", proxy.GetResult()); proxy.Subtract(2);
17: Console.WriteLine("Subtract(2)", proxy.GetResult()); Console.WriteLine("運算結果為: {0}", proxy.GetResult());
18: } Console.Read();
19: }
20: }
21: }
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint address="http://localhost/AspCompatibleServices/CalculatorService.svc"
6: binding="wsHttpBinding" contract="Artech.AspCompatibleServices.Contracts.ICalculator"
7: name="CalculatorService"/>
8: </client> </system.serviceModel>
9: </configuration>
但是,但我們運行客戶端的程序,輸出的結果並不像我們希望的那樣。從下面的結果可以看出,每次通過GetResult()方法得到的結果都是0,也就是說,服務端並沒有將運算結果保存下來。
1: 初始值為:0
2: Add(3)運算結果為:0
3: Multiply(10)運算結果為:0
4: Subtract(2)運算結果為:0
允許Cookie傳遞
要解釋這個問題,得從Session的實現機制說起。眾所周知,HTTP是無狀態(Stateless)的傳輸協議,對服務端來說,它收到的每個HTTP請求都是全新的請求。ASP.NET會話(Session)的實現很簡單,就是讓每次HTTP請求攜帶Session的識別信息(Session ID),那麼服務就可以根據此信息判斷請求來自哪個客戶端了。關於Session識別信息的保存,ASP.NET有兩種方式:Cookie和URL,前者將其放到Cookie中,每次HTTP請求將會攜帶該Cookie的值,後者則將其作為請求URL的一部分。一般情況下采用基於Cookie的實現機制,如果Cookie禁用則采用後者。
那麼對於ASP.NET兼容模式下的WCF也一樣,要想讓服務端能夠識別會話,就需要讓每個服務調用的HTTP請求攜帶Session的識別信息,我們也可以通過傳遞Cookie的方式來解決這個問題。對於WCF來說,Cookie傳遞能夠通過Binding來控制,對於WsHttpBinding來說,默認情況下並不允許Cookie的傳遞。我們可以通過WsHttpBinding的AllowCookies來控制是否允許傳遞Cookie,該屬性可以通過配置進行設置。為此,我們對客戶端的配置進行了如下的修改。再次運行我們的案例程序,將會得到你期望的輸出。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint address="http://localhost/AspCompatibleServices/CalculatorService.svc"
6: binding="wsHttpBinding" contract="Artech.AspCompatibleServices.Contracts.ICalculator"
7: name="CalculatorService" bindingConfiguration="CookieAllowableBinding"/>
8: </client>
9: <bindings>
10: <wsHttpBinding>
11: <binding name="CookieAllowableBinding" allowCookies="true"/>
12: </wsHttpBinding>
13: </bindings>
14: </system.serviceModel>
15: </configuration>
客戶端輸出結果:
1: 初始值為:0
2: Add(3)運算結果為:3
3: Multiply(10)運算結果為:30
4: Subtract(2)運算結果為:28
隨文源碼:http://www.bianceng.net/dotnet/201210/531.htm