WCF技术剖析之三十一: WCF事务编程[中篇]

时间:2022-04-22
本文章向大家介绍WCF技术剖析之三十一: WCF事务编程[中篇],主要内容包括一、绑定对事务流转的支持、二、 绑定与TransactionFlow设置、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

[续《上篇》]通过将TransactionFlowAttribute特性应用在服务契约的某个操作之上,并指定相应的TransactionFlowOption枚举直,仅仅定义了事务流转的策略而已。或者说,通过这种方式确定对事物流转的一种意愿,客户端是否愿意将当前事务流出,服务端是否愿意接受流入的事务,可以通过TransactionFlowAttribute特性进行控制。所以说,服务操作上定义个TransactionFlowAttribute特性是是否进行事务流转的总开关,真正的事务传播是建立在TransactionFlowOption.Allowed或者TransactionFlowOption.Mandatory之上的。

至于WCF框架是否有能力对事物进行流转,按照怎样的协议进行流转,则是通过绑定实现的,现在我们首先看看怎样的绑定具有事务流转的能力。

一、绑定对事务流转的支持

WCF技术剖析(卷1)》中的第3章对绑定的本质进行了深层次的剖析,阅读过本章的读者应该知道:绑定是一系列绑定元素(BindingElement)的有序组合,相应的绑定元素对消息进行相应的处理以实现特定的目标,比如MessageEncodingBindingElement实现对消息的编码和解码,TransportBindingElement实现对消息的传输。

消息交换是WCF进行通信的唯一手段,任何需要传输的数据最终都需要最为消息的一部分。对象事务流转来说,客户端需要将当前事务进行序列化并嵌入到消息中;服务端则需要从接收到的消息中提取事务相关信息,反序列化以重建事务。这样的操作同样实现在一个绑定元素中,即TransactionFlowBindingElement

既然TransactionFlowBindingElement实现了对事物的流转,那么我们就可以根据某个绑定对象的绑定元素集合中是否包含该元素判断绑定是否支持事务流转。为此,我写了如下一个简单的方法,传入相应的Binding对象,打印出相应的绑定类型是否支持事务流转:

   1: static void PrintTransactionFlowSupport(Binding binding)
   2: {
   3:     TransactionFlowBindingElement transactionFlowElement = binding.CreateBindingElements().Find< TransactionFlowBindingElement>();
   4:     Console.WriteLine("{0,-30} {1}",binding.GetType().Name,transactionFlowElement!=null?"Yes":"No");
   5: }

现在,我们通过调用PrintTransactionFlowSupport方法,判断所有的系统绑定是否为事务流转提供支持。从输出结果来看,除了BasicHttpBindingNetMsmqBindingMsmqIntegrationBinding三种,其余的系统绑定均包含TransactionFlowBindingElement绑定元素,也就是说它们均具有对事务就是传播的能力。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine("{0,-30} {1}", "Binding", "Transaction Flow");
   6:         Console.WriteLine("--------------------------------------------");
   7:         //BasicHttpBinding
   8:         PrintTransactionFlowSupport(new BasicHttpBinding());
   9:  
  10:         //WS Binding
  11:         PrintTransactionFlowSupport(new WSHttpBinding());
  12:         PrintTransactionFlowSupport(new WS2007HttpBinding());
  13:         PrintTransactionFlowSupport(new WSDualHttpBinding());
  14:         PrintTransactionFlowSupport(new WSFederationHttpBinding());
  15:         PrintTransactionFlowSupport(new WS2007FederationHttpBinding());
  16:  
  17:         //TCP and IPC Binding
  18:         PrintTransactionFlowSupport(new NetTcpBinding());
  19:         PrintTransactionFlowSupport(new NetNamedPipeBinding());
  20:         //MSMQ Binding
  21:         PrintTransactionFlowSupport(new NetMsmqBinding());
  22:         PrintTransactionFlowSupport(new MsmqIntegrationBinding());
  23:     }
  24: }

输出结果:

Binding                            Transaction Flow
-----------------------------------------------
BasicHttpBinding                   No
WSHttpBinding                      Yes
WS2007HttpBinding                  Yes
WSDualHttpBinding                  Yes
WSFederationHttpBinding            Yes
WS2007FederationHttpBinding        Yes
NetTcpBinding                      Yes
NetNamedPipeBinding                Yes
NetMsmqBinding                     No
MsmqIntegrationBinding             No

由于BasicHttpBinding基于WS-I Basic Profile标准的绑定,而两个基于MSQM的绑定(NetMsmqBindingMsmqIntegrationBinding)只能采用单向(One-Way)的消息交换模式,所以它们不具有事务流转的能力。但是,即使对于契约的支持事务的绑定类型,事务流转默认也是被关闭的,在真正需要事先事务流转的场景中,需要通过配置或者编成的方式开启该选项。此外,事务流转涉及事务在消息中的格式化问题,而事务的格式化决定于采用的协议。通过《谈谈分布式事务之四: 两种事务处理协议OleTx与WS-AT》我们知道,WCF支持三种不同的事务处理协议:OleTx,WS-AT 1.0和WS-AT 1.0。事务处理协议通过类型TransactionProtocol类型表示,TransactionProtocol定义如下:

   1: public abstract class TransactionProtocol
   2: {    
   3:     public static TransactionProtocol Default { get; }
   4:  
   5:     public static TransactionProtocol OleTransactions { get; }
   6:     public static TransactionProtocol WSAtomicTransactionOctober2004 { get; }
   7:     public static TransactionProtocol WSAtomicTransaction11 { get; }
   8: }

TransactionProtocol是一个抽象类,定义了三种静态只读属性OleTransactions、WSAtomicTransactionOctober2004和WSAtomicTransaction11,用于获取分别代表OleTx,WS-AT 1.0和WS-AT 1.0三种协议的具体TransactionProtocol对象。这三种具体的TransactionProtocol类型以内部(Internal)类型的方式定义。Default制度属性返回默认的事务处理协议,和OleTransactions属性值一致。

对于NetTcpBindingNetNamedPipeBinding来说,我们可以通过属性TransactionFlow设置或者获取绑定是否支持事务流转的开关,并通过TransactionProtocol属性设置或者获取绑定支持的事务处理协议。

   1: public class NetTcpBinding : Binding, IBindingRuntimePreferences
   2: {
   3:     //其他成员
   4: public bool TransactionFlow { get; set; }
   5: public TransactionProtocol TransactionProtocol { get; set; }
   6: }
   7:  
   8: public class NetNamedPipeBinding : Binding, IBindingRuntimePreferences
   9: {
  10:     //其他成员
  11:     public bool TransactionFlow { get; set; }
  12:     public TransactionProtocol TransactionProtocol { get; set; }
  13: }

而对于基于WS的绑定来说,由于绑定本身就是为跨平台和互操作涉及的,所以仅仅支持基于WS-AT的事务处理协议,其中WSHttpBindingWSDualHttpBindingWSFederationHttpBinding支持的协议是WS-AT 1.0,而WS2007HttpBindingWS2007FederationHttpBinding支持的是WS-AT 1.1。所以,它们仅仅具有TransactionFlow属性,并没有TransactionProtocol属性,该属性定义在它们的基类WSHttpBindingBase上面:

   1: public abstract class WSHttpBindingBase : Binding, IBindingRuntimePreferences
   2: {
   3:     //其他成员
   4:     public bool TransactionFlow { get; set; }
   5: }

系统绑定的TransactionFlow和TransactionProtocol属性(仅限于NetTcpBindingNetNamedPipeBinding)可以通过配置的方式指定。下面的配置中定义了开启了transactionFlow开关的两个绑定(NetTcpBinding和WS2007HttpBinding),并将其中的NetTcpBinding的TransactionProtocol设置成基于WS-AT 1.0的协议(transactionProtocol="WSAtomicTransactionOctober2004")。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <bindings>
   5:             <netTcpBinding>
   6:                 <binding name="transactionalTcpBinding" transactionFlow="true" transactionProtocol="WSAtomicTransactionOctober2004" />
   7:                 </netTcpBinding>
   8:           <ws2007HttpBinding>
   9:             <binding name="transactionalHttpBinding" transactionFlow="true" />
  10:           </ws2007HttpBinding>
  11:         </bindings>
  12:         <services>
  13:             <service name="Artech.TransactionalServices.BankingService">
  14:                 <endpoint address="net.tcp://127.0.0.1/bankingservice" binding="netTcpBinding"
  15:                     bindingConfiguration="transactionalTcpBinding" contract="Artech.TransactionalServices.IBankingService" />
  16:               <endpoint address="http://127.0.0.1/bankingservice" binding="ws2007HttpBinding"
  17:       bindingConfiguration="transactionalHttpBinding" contract="Artech.TransactionalServices.IBankingService" />
  18:             </service>
  19:         </services>
  20:     </system.serviceModel>
  21: </configuration>

如果现有的系统绑定不能满足你的需要(比如你需要同时采用HTTP传输协议和OleTx事务处理协议),可以通过编程或者配置的方式创建自定的绑定(CustomBinding)。创建支持事务流转的自定义绑定的时候,你需要做的仅仅是将TransactionFlowBindingElement添加到绑定元素集合中,并设置TransactionFlow和TransactionProtocol属性即可。下面的配置就定义了这样一个基于OleTx的HTTP绑定。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>
   4:         <bindings>
   5:             <customBinding>
   6:                 <binding name="transactionalBinding">
   7:                     <textMessageEncoding />
   8:                     <transactionFlow transactionProtocol="OleTransactions"/>
   9:                     <httpTransport />
  10:                 </binding>
  11:             </customBinding>
  12:         </bindings>
  13:         <services>
  14:             <service name="Artech.TransactionalServices.BankingService">
  15:                 <endpoint address="http://127.0.0.1/bankingservice" binding="customBinding"
  16:                     bindingConfiguration="transactionalBinding" contract="Artech.TransactionalServices.IBankingService" />
  17:             </service>
  18:         </services>
  19:     </system.serviceModel>
  20: </configuration>

二、 绑定与TransactionFlow设置

通过应用TransactionFlowAttribute特性为某个操作设置相应的事务流转策略,绑定决定了实现事务流转的能力和方式,两个的不同组合表现出不同的事务流转行为。在这里,事务的流转包含两个层面的意思,即事务的流出或者发送,以及事务的流入或者接收。对于WCF的客户端框架来说,对于通过TransactionFlowAttribute特性设置的三个选项来说,NotAllowed和Allowed对绑定的事务流转能力没有任何要求,而Madantory则强制要求终结点的绑定能够实现事务的流转(绑定本身能够支持事务流转并且TransactionFlow开关必须开启)。结合上面所介绍的,事务流转选项和绑定类型两两组合所表现出的行为如下面的表格所示(这里的事务绑定表示TransactionFlow开关开启的支持事务流转的绑定)。

事务绑定

非事务绑定

NotAllowed

当前事务不需要存在,存在的当前事务不会被流出

当前事务不需要存在,存在的当前事务不会被流出

Allowed

当前事务不需要存在,存在的当前事务会被流出

当前事务不需要存在,存在的当前事务不会被流出

Mandatory

当前事务必须存在,存在的当前事务会被流出

不合法的组合

对于一个服务契约来说,如果任何一个操作的TransactionFlow选项被定义成Mandatory,相应终结点所采用的绑定必须是事务绑定(接下来我们将本身支持事务流转,并开启了TransactionFlow开关的绑定称为事务绑定)。下面的代码和配置中,通过TransactionFlowAttribute将唯一的Transfer操作的事务流转选项设置为Mandatory,并选用不支持事务流转的BasicHttpBinding。当使用创建的ChannelFactory<TChannel>创建服务代理的时候,抛出如图1所示的InvalidOperationException异常。

   1: [ServiceContract(Namespace="http://www.artech.com/")]
   2: public interface IBankingService
   3: {
   4:     [OperationContract]
   5:     [TransactionFlow(TransactionFlowOption.Mandatory)]
   6:     void Transfer(string accountFrom, string accountTo, double amount);
   7: }
   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>    
   4:     <client>
   5:       <endpoint address="http://127.0.0.1:3721/bankingservice" binding="basicHttpBinding" contract="Artech.TransactionalServices.IBankingService" name="bankingservice" />
   6:     </client>    
   7:   </system.serviceModel>
   8: </configuration>

图1 客户端在Mandatory事务流转选项情况下采用非事务绑定抛出的异常

上面所说的是不同的事务流转选项和绑定类型在客户端的表现行为,现在我们将目光转移到服务端。较之客户端,服务的情况要稍微复杂一些,处理考虑事务流转选项和绑定对事务流转的支持之外,还需要考虑以下三个因素:

  • 接收的消息中是否具有包含流入事务的SOAP报头;
  • 如果包括需要考虑流入事务在SOAP报头中的XML格式是否与绑定采用的事务处理协议一致;
  • 如果不一致需要考虑事务报头的MustUnderstand属性是True(或1)还是False(或者0)。

WCF服务端服务流转表现出来的最终行为决定于上述的五个要素,下面的表格流出了它们之间不同的组合最终表现出来的事务处理行为。

首先,如果一个服务契约的任何一个操作的TransactionFlow选项定义成Mandatory,那么强制要求相应的终结点采用事务绑定。比如说,同样对于上面定义的IBankingService服务契约(TransactionFlow),但是使用默认的WS2007HttpBinding(默认情况下TransactionFlow是关闭的),在进行服务寄宿的时候,会抛出如图2所示的InvalidOperationException异常。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>      
   4:         <services>
   5:             <service name="Artech.TransactionalServices.BankingService">
   6:                 <endpoint address="http://127.0.0.1:3721/bankingservice" binding="ws2007HttpBinding" contract="Artech.TransactionalServices.IBankingService" />
   7:             </service>
   8:         </services>
   9:     </system.serviceModel>
  10: </configuration>

图2 客户端在Mandatory事务流转选项情况下采用非事务绑定抛出的异常

其次,同样对于TransactionFlow选项为Mandatory的操作,如果接收的消息并不包含流入事务的SOAP报头,或者说流入的事务在SOAP报头中的表示并不符合绑定采用的事务处理协议,由于Mandatory选项在服务端的含义就是强制需要流入一个可以理解的事务,在这种情况下服务端会返回一个Fault消息,并导致客户端抛出异常。同样是对于前面给定义的IBankingService服务契约,如果我们将客户端和服务端终结点的绑定配置成不同的事务处理协议,比如客户端采用默认的OleTx,服务端则采用WS-AT 1.1。客户端在进行服务调用的时候,会抛出如图3所示的ProtocolException异常。

客户端配置:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <netTcpBinding>
   6:         <binding name="nonTransactionalBinding" transactionFlow="true"/>
   7:       </netTcpBinding>
   8:     </bindings>
   9:     <client>
  10:       <endpoint address="net.tcp://127.0.0.1:3721/bankingservice" binding="netTcpBinding" bindingConfiguration="nonTransactionalBinding" contract="Artech.TransactionalServices.IBankingService" name="bankingservice" />
  11:     </client>    
  12:   </system.serviceModel>
  13: </configuration>

服务端配置:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>      
   4:         <bindings>
   5:             <netTcpBinding>
   6:                 <binding name="transactionalBinding" transactionFlow="true" transactionProtocol="WSAtomicTransaction11" />
   7:             </netTcpBinding>
   8:         </bindings>
   9:         <services>
  10:             <service name="Artech.TransactionalServices.BankingService">
  11:                 <endpoint address="net.tcp://127.0.0.1:3721/bankingservice" binding="netTcpBinding"
  12:                     bindingConfiguration="transactionalBinding" contract="Artech.TransactionalServices.IBankingService" />
  13:             </service>
  14:         </services>
  15:     </system.serviceModel>
  16: </configuration>

图3 客户端端和服务端采用不同的事务处理协议导致的异常(Mandatory)

倘若接收到的消息中存在事务报头,并且报头的MustUnderstand属性为True或者1,对于Allowed选项来说,如果采用非事务绑定,或者说虽然采用事务绑定,但是事务报头与绑定采用的事务处理协议不符。在这种情况下,服务端不能有效地理解事务报头,也会向客户端返回一个Fault消息,并导致客户端抛出异常。比如说,我们采用上面提供的配置(客户端和服务端绑定采用不同的事务处理协议),如果我们将服务契约IBankingService的Transfer操作的TransactionFlow选项设置为Allowed,客户端在进行服务调用的时候会抛出如图4所示的ProtocolException异常。但是,如果MustUnderstand属性为False或者0,事务报头会被忽略。

   1: [ServiceContract(Namespace = "http://www.artech.com/")]
   2: public interface IBankingService
   3: {
   4:     [OperationContract]
   5:     [TransactionFlow(TransactionFlowOption.Allowed)]
   6:     void Transfer(string accountFrom, string accountTo, double amount);
   7: }

图4 客户端端和服务端采用不同的事务处理协议导致的异常(Allowed)

相似情况同样发生在TransactionFlow选项为NotAllowed的时候,所不同的是:即使接收到的事务报头与绑定采用的事务处理协议相匹配,仍然会导致事务报头不能理解的异常。