WCF技术剖析之三十:一个很有用的WCF调用编程技巧

1/6/2010来源:ASP技巧人气:4555

在进行基于会话信道的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参数,因为这两种类型的参数不能作为匿名方法的参数。