在進行基於會話通道的 WCF 服務調用時,受限於並發通道數量,必須及時關閉通道;遇到特定異常則需強制中止。正如我們不讓開發人員手動管理資料庫連接,也不應讓他們手動創建、開啟、中止和關閉通道——這些基礎設施工作應由框架自動完成。本文介紹如何透過程式設計技巧,讓開發者忽略「通道」的存在,像呼叫普通物件一樣進行服務調用。
若透過 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();
}
}
這種寫法雖正確,但若在每處調用都重複,會導致程式碼臃腫、大量重複,且頻繁建立 ChannelFactory<TChannel> 影響效能。
我們可建立公共方法統一處理重複邏輯:ChannelFactory 的建立、快取、服務代理的創建/中止/關閉、異常處理。以下 ServiceInvoker 類別提供兩個靜態 Invoke 多載,分別對應無回傳值與有回傳值的服務操作。
using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace Artech.Lib
{
public class ServiceInvoker
{
private static Dictionary<string, ChannelFactory> channelFactories = new Dictionary<string, ChannelFactory>();
private static object syncHelper = new object();
private static ChannelFactory<TChannel> GetChannelFactory<TChannel>(string endpointConfigurationName)
{
ChannelFactory<TChannel> channelFactory = null;
if (channelFactories.ContainsKey(endpointConfigurationName))
{
channelFactory = channelFactories[endpointConfigurationName] as ChannelFactory<TChannel>;
}
if (null == channelFactory)
{
channelFactory = new ChannelFactory<TChannel>(endpointConfigurationName);
lock (syncHelper)
{
channelFactories[endpointConfigurationName] = channelFactory;
}
}
return channelFactory;
}
public static void Invoke<TChannel>(Action<TChannel> action, TChannel proxy)
{
ICommunicationObject channel = proxy as ICommunicationObject;
if (null == channel)
{
throw new ArgumentException("The proxy is not a valid channel implementing the ICommunicationObject interface", "proxy");
}
try
{
action(proxy);
}
catch (TimeoutException)
{
channel.Abort();
throw;
}
catch (CommunicationException)
{
channel.Abort();
throw;
}
finally
{
channel.Close();
}
}
public static TResult Invoke<TChannel, TResult>(Func<TChannel, TResult> function, TChannel proxy)
{
ICommunicationObject channel = proxy as ICommunicationObject;
if (null == channel)
{
throw new ArgumentException("The proxy is not a valid channel implementing the ICommunicationObject interface", "proxy");
}
try
{
return function(proxy);
}
catch (TimeoutException)
{
channel.Abort();
throw;
}
catch (CommunicationException)
{
channel.Abort();
throw;
}
finally
{
channel.Close();
}
}
public static void Invoke<TChannel>(Action<TChannel> action, string endpointConfigurationName)
{
Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
Invoke<TChannel>(action, GetChannelFactory<TChannel>(endpointConfigurationName).CreateChannel());
}
public static TResult Invoke<TChannel, TResult>(Func<TChannel, TResult> function, string endpointConfigurationName)
{
Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
return Invoke<TChannel, TResult>(function, GetChannelFactory<TChannel>(endpointConfigurationName).CreateChannel());
}
}
}
為避免頻繁建立 ChannelFactory,透過字典快取已建立的實例。服務調用以委派(Action<TChannel>、Func<TChannel, TResult>)表示,呼叫端只需傳入端點組態名稱,調用碼瞬間簡潔:
using System;
using Artech.Lib;
using Artech.WcfServices.Contracts;
namespace Artech.WcfServices.Clients
{
class Program
{
static void Main(string[] args)
{
int result = ServiceInvoker.Invoke<ICalculator, int>(calculator => calculator.Add(1, 2), "calculatorservice");
Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, result);
Console.Read();
}
}
}
為進一步封裝調用細節,通常會建立「服務代理層」。將 ServiceInvoker 改為泛型類別 ServiceInvoker<TChannel>,繼承原靜態類別,並提供實例方法。再定義基類 ServiceProxyBase<TChannel>,持有 ServiceInvoker<TChannel> 實例,具體代理類別繼承此基類並實作服務契約介面,內部委派給 Invoker 執行。
using System;
namespace Artech.Lib
{
public class ServiceInvoker<TChannel> : ServiceInvoker
{
public string EndpointConfigurationName { get; private set; }
public ServiceInvoker(string endpointConfigurationName)
{
Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
this.EndpointConfigurationName = endpointConfigurationName;
}
public void Invoke(Action<TChannel> action)
{
Invoke<TChannel>(action, this.EndpointConfigurationName);
}
public TResult Invoke<TResult>(Func<TChannel, TResult> function)
{
return Invoke<TChannel, TResult>(function, this.EndpointConfigurationName);
}
}
}
namespace Artech.Lib
{
public class ServiceProxyBase<TChannel>
{
public virtual ServiceInvoker<TChannel> Invoker { get; private set; }
public ServiceProxyBase(string endpointConfigurationName)
{
Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
this.Invoker = new ServiceInvoker<TChannel>(endpointConfigurationName);
}
}
}
具體代理範例:
using Artech.Lib;
using Artech.WcfServices.Contracts;
namespace Artech.WcfServices.Clients
{
public class CalculatorProxy : ServiceProxyBase<ICalculator>, ICalculator
{
public CalculatorProxy() : base(Constants.EndpointConfigurationNames.CalculatorService) { }
public int Add(int x, int y)
{
return this.Invoker.Invoke<int>(calculator => calculator.Add(x, y));
}
}
public class Constants
{
public class EndpointConfigurationNames
{
public const string CalculatorService = "calculatorservice";
}
}
}
消費端(如 Presenter 層)即可直接實例化代理並呼叫方法,所有通道管理細節均被封裝:
using System;
using Artech.Lib;
using Artech.WcfServices.Contracts;
namespace Artech.WcfServices.Clients
{
class Program
{
static void Main(string[] args)
{
CalculatorProxy calculatorProxy = new CalculatorProxy();
int result = calculatorProxy.Add(1, 2);
Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, result);
Console.Read();
}
}
}
此方案一大限制:服務操作不可包含 ref 或 out 參數,因為這兩類參數無法作為匿名方法的參數。