在進行基於會話信道的WCF服務調用中,由於受到並發信道數量的限制,我們需要及時的關閉信道;當遇到某些異常,我們需要強行中止(Abort)信道,相關的原理,可以參考我的文章《服務代理不能得到及時關閉會有什麼後果?》。在真正的企業級開發中,正如我們一般不會讓開發人員手工控制數據庫連接的開啟和關閉一樣,我們一般也不會讓開發人員手工去創建、開啟、中止和關閉信道,這些工作是框架應該完成的操作。這篇文章,我們就來介紹如果通過一些編程技巧,讓開發者能夠無視“信道”的存在,像調用一個普通對象一樣進行服務調用。
一、正常的服務調用方式
如果通過ChannelFactory<TChannel>創建用於服務調用的代理,下面的代碼片段描述了客戶端典型的服務調用形式:將服務調用在基於代理對象的using塊中,並通過try/catch進一步對服務調用操作進行異常處理。當TimeoutException或者CommunicationException被捕獲後,調用Abort方法將信道中止。當程序執行到using的末尾,Dispose方法會進一步調用Close方法對信道進行關閉。
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
{
ICalculator calculator = channelFactory.CreateChannel();
using (calculator as IDisposable)
{
try
{
Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2));
}
catch (TimeoutException)
{
(calculator as ICommunicationObject).Abort();
throw;
}
catch (CommunicationException)
{
(calculator as ICommunicationObject).Abort();
throw;
}
}
}
Console.Read();
}
}
二、借助通過Delegate實現異常處理和服務代理的關閉
雖然上面的編程方式是正確的服務調用方式,但是在真正的應用中,如果在每處進行服務調用的地方都采用上面的方式,在我看來是不能容忍的。這不但會讓你的程序顯得臃腫不堪,而且帶來非常多重復的代碼,此外頻繁創建ChannelFactory<TChannel>對性能也會有影響。我們可以通過一些公共個方法實現對重復代碼(ChannelFactory<TChannel>的創建,服務調用的創建、中止和關閉,以及異常處理)。為此我創建了如下一個ServiceInvoker類型,通過兩個重載的Invoke方法實現對目標服務的調用。
1: using System;
2: using System.Collections.Generic;
3: using System.ServiceModel;
4: namespace Artech.Lib
5: {
6: public class ServiceInvoker
7: {
8: private static Dictionary<string, ChannelFactory> channelFactories = new Dictionary<string, ChannelFactory>();
9: private static object syncHelper = new object();
10:
11: private static ChannelFactory<TChannel> GetChannelFactory<TChannel>(string endpointConfigurationName)
12: {
13: ChannelFactory<TChannel> channelFactory = null;
14: if (channelFactories.ContainsKey(endpointConfigurationName))
15: {
16: channelFactory = channelFactories[endpointConfigurationName] as ChannelFactory<TChannel>;
17: }
18:
19: if (null == channelFactory)
20: {
21: channelFactory = new ChannelFactory<TChannel>(endpointConfigurationName);
22: lock (syncHelper)
23: {
24: channelFactories[endpointConfigurationName] = channelFactory;
25: }
26: }
27: return channelFactory;
28: }
29:
30: public static void Invoke<TChannel>(Action<TChannel> action, TChannel proxy)
31: {
32: ICommunicationObject channel = proxy as ICommunicationObject;
33: if (null == channel)
34: {
35: throw new ArgumentException("The proxy is not a valid channel implementing the ICommunicationObject interface", "proxy");
36: }
37: try
38: {
39: action(proxy);
40: }
41: catch (TimeoutException)
42: {
43: channel.Abort();
44: throw;
45: }
46: catch (CommunicationException)
47: {
48: channel.Abort();
49: throw;
50: }
51: finally
52: {
53: channel.Close();
54: }
55: }
56:
57: public static TResult Invoke<TChannel, TResult>(Func<TChannel, TResult> function, TChannel proxy)
58: {
59: ICommunicationObject channel = proxy as ICommunicationObject;
60: if (null == channel)
61: {
62: throw new ArgumentException("The proxy is not a valid channel implementing the ICommunicationObject interface", "proxy");
63: }
64: try
65: {
66: return function(proxy);
67: }
68: catch (TimeoutException)
69: {
70: channel.Abort();
71: throw;
72: }
73: catch (CommunicationException)
74: {
75: channel.Abort();
76: throw;
77: }
78: finally
79: {
80: channel.Close();
81: }
82: }
83:
84: public static void Invoke<TChannel>(Action<TChannel> action, string endpointConfigurationName)
85: {
86: Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
87: Invoke<TChannel>(action, GetChannelFactory<TChannel>(endpointConfigurationName).CreateChannel());
88: }
89:
90: public static TResult Invoke<TChannel, TResult>(Func<TChannel, TResult> function, string endpointConfigurationName)
91: {
92: Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
93: return Invoke<TChannel, TResult>(function, GetChannelFactory<TChannel>(endpointConfigurationName).CreateChannel());
94: }
95: }
96: }
處於對性能的考慮,避免對ChannelFactory<TChannel>的頻繁創建,通過一個字典對象將創建出來的ChannelFactory<TChannel>緩存起來;兩個Invoke方法中,服務的調用通過兩個Delegate對象(Action<TChannel>和Func<TChannel, TResult>)表示,另一個參數表示終結點的配置名稱。那麼這時的服務調用就會變得相當簡單:
1: using System;
2: using Artech.Lib;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: int result = ServiceInvoker.Invoke<ICalculator, int>(calculator => calculator.Add(1, 2), "calculatorservice");
11: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, result);
12: Console.Read();
13: }
14: }
15: }
三、對ServiceInvoker的改進
實際上,為了對服務調用實現細節進行進一步的封裝,一般地我們可以將其定義在一個獨立的層中,比如服務代理層(這裡的層不一定像數據訪問層、業務邏輯層一樣需要一個明顯的界限,這裡可能就是一個單獨的類型而已)。在這種情況下,我們可以上面的ServiceInvoker方法進行一定的改造,使之更加符合這種分層的場景。上面我們調用靜態方法的形式進行服務的調用,現在我們需要的是:實例化服務代理對象,並調用相應的方法。為此,我創建了一個泛型的ServiceInvoker<TChannel>類型,該類型繼承自上述的ServiceInvoker,泛型類型表示服務契約類型。ServiceInvoker<TChannel>定義如下:
1: using System;
2: namespace Artech.Lib
3: {
4: public class ServiceInvoker<TChannel>:ServiceInvoker
5: {
6: public string EndpointConfigurationName
7: {get; private set;}
8:
9: public ServiceInvoker(string endpointConfigurationName)
10: {
11: Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
12: this.EndpointConfigurationName = endpointConfigurationName;
13: }
14:
15: public void Invoke(Action<TChannel> action)
16: {
17: Invoke<TChannel>(action, this.EndpointConfigurationName);
18: }
19:
20: public TResult Invoke<TResult>(Func<TChannel, TResult> function)
21: {
22: return Invoke<TChannel, TResult>(function, this.EndpointConfigurationName);
23: }
24: }
25: }
通過傳入終結點配置名稱創建ServiceInvoker<TChannel>對象,直接通過調用基類的靜態方法實現了兩個Invoke方法。
在分層設計中,為每一個層定義的組件創建基類是一個很常見的設計方式。在這裡,假設所有的服務代理類型均繼承自基類:ServiceProxyBase<TChannel>,泛型類型為服務契約類型。同樣通過傳入終結點配置名稱創建服務代理,並借助於通過Invoker屬性表示的ServiceInvoker<TChannel>對象進行服務的調用。ServiceProxyBase<TChannel>定義如下:
1: namespace Artech.Lib
2: {
3: public class ServiceProxyBase<TChannel>
4: {
5: public virtual ServiceInvoker<TChannel> Invoker
6: { get; private set; }
7:
8: public ServiceProxyBase(string endpointConfigurationName)
9: {
10: Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
11: this.Invoker = new ServiceInvoker<TChannel>(endpointConfigurationName);
12: }
13: }
14: }
那麼,具體的服務代理類型就可以通過如下的方式定義了:
1: using Artech.Lib;
2: using Artech.WcfServices.Contracts;
3: namespace Artech.WcfServices.Clients
4: {
5: public class CalculatorProxy : ServiceProxyBase<ICalculator>, ICalculator
6: {
7: public CalculatorProxy():base(Constants.EndpointConfigurationNames.CalculatorService)
8: { }
9:
10: public int Add(int x, int y)
11: {
12: return this.Invoker.Invoke<int>(calculator => calculator.Add(x, y));
13: }
14: }
15:
16: public class Constants
17: {
18: public class EndpointConfigurationNames
19: {
20: public const string CalculatorService = "calculatorservice";
21: }
22: }
23: }
那麼現在服務代理的消費者(一般是Presenter層對象),就可以直接實例化服務代理對象,並調用相應的方法(這裡的方法與服務契約方法一致)即可,所有關於服務調用的細節均被封裝在服務代理中。
1: using System;
2: using Artech.Lib;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: CalculatorProxy calculatorProxy = new CalculatorProxy();
11: int result = calculatorProxy.Add(1, 2);
12: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, result);
13: Console.Read();
14: }
15: }
16: }
四、局限
這個解決方案有一個很大的局限:服務方式不能包含ref和out參數,因為這兩種類型的參數不能作為匿名方法的參數。