Section 7.10. Callbacks


7.10. Callbacks

Callback contracts, just like service contracts, can propagate the service transaction to the callback client. You apply the transactionFlow attribute, as with a service contract, for example:

 interface IMyContractCallback {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Allowed)]    void OnCallback( ); } [ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract {...} 

The callback method implementation can use the OperationBehavior attribute just like a service operation and specify requiring a transaction scope and auto-completion:

 class MyClient : IMyContractCallback {    [OperationBehavior(TransactionScopeRequired = true)]    public void OnCallback( )    {       Transaction transaction = Transaction.Current;       Debug.Assert(transaction != null);    } } 

7.10.1. Callback Transaction Modes

The callback client can have four modes of configuration: Service, Service/Callback, Callback, and None, analogous to the service transaction modes, except the service now plays the client role and the callback plays the service role in the previous service-side modes. For example, to configure the callback for Service transaction mode (that is, always using the service transaction), follow these steps:

  1. Use a transaction-aware duplex binding with transaction flow enabled.

  2. Set transaction flow to mandatory on the callback operation.

  3. Configure the callback operation to require a transaction scope.

Example 7-25 shows a callback client configured for Service transaction.

Example 7-25. Configuring the callback for Service transaction

 interface IMyContractCallback {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Mandatory)]    void OnCallback( ); } class MyClient : IMyContractCallback {    [OperationBehavior(TransactionScopeRequired = true)]    public void OnCallback( )    {       Transaction transaction = Transaction.Current;       Debug.Assert(transaction.TransactionInformation.                    DistributedIdentifier != Guid.Empty);    } } 

When the callback operation is configured for mandatory transaction flow, WCF will enforce the use of a transaction-ware binding with transaction flow enabled.

When configuring for Service/Callback transaction propagation mode, WCF does not enforce the use of a transaction aware binding or that transaction flow is enabled. You can use my BindingRequirement attribute to verify this:

 interface IMyContractCallback {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Allowed)]    void OnCallback( ); } [BindingRequirement(TransactionFlowEnabled = true)] class MyClient : IMyContractCallback {    [OperationBehavior(TransactionScopeRequired = true)]    public void OnCallback( )    {...} } 

I extended the BindingRequirement attribute to verify callback binding by implementing the IEndpointBehavior interface:

 public interface IEndpointBehavior {    void AddBindingParameters(ServiceEndpoint endpoint,                              BindingParameterCollection bindingParameters);    void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,                             ClientRuntime behavior);    void ApplyDispatchBehavior(ServiceEndpoint endpoint,                               EndpointDispatcher endpointDispatcher);    void Validate(ServiceEndpoint serviceEndpoint); } 

As explained in Chapter 6, the IEndpointBehavior interface lets you configure the client-side endpoint used for the callback by the service. In the case of the BindingRequirement attribute, it uses the IEndpointBehavior.Validate( ) method, and the implementation is almost identical to that of Example 7-3.

7.10.1.1. Isolation and timeouts

Similar to a service, the CallbackBehavior attribute enables a callback type to control its transaction's timeout and isolation level:

 [AttributeUsage(AttributeTargets.Class)] public sealed class CallbackBehaviorAttribute: Attribute,IEndpointBehavior {    public IsolationLevel TransactionIsolationLevel    {get;set;}    public string TransactionTimeout    {get;set;}    //More members } 

These properties accept the same values as in the service case, and choosing a particular value follows the same reasoning.

7.10.2. Callback Voting

By default, WCF will use automatic voting for the callback operation, just as with a service operation. Any exception in the callback will vote to abort the transaction, and without an error WCF will vote to commit the transaction, as is the case in Example 7-25. However, unlike a service instance, the callback instance life cycle is managed by the client, and it has no instancing mode. Any callback instance can be configured for explicit voting by setting TRansactionAutoComplete to false, and then voting explicitly using SetTransactionComplete( ):

 class MyClient : IMyContractCallback {    [OperationBehavior(TransactionScopeRequired = true,                       TransactionAutoComplete = false)]    public void OnCallback( )    {       /* Do some transactional work then */       OperationContext.Current.SetTransactionComplete( );    } } 

As with a per-session service, explicit voting is for the case when the vote depends on other things besides exceptions. Do not perform any work, especially transactional work after the call to SetTransactionComplete( ). Calling SetTransactionComplete( ) should be the last line of code in the callback operation just before returning. If you try to perform any transactional work (including accessing transaction.Current) after the call to SetTransactionComplete( ), WCF will throw an InvalidOperationException and abort the transaction.

7.10.3. Using Transactional Callbacks

While WCF provides the infrastructure for propagating the service transaction to the callback, in reality callbacks and service transactions do not mix well. First, callbacks are usually one-way operations, and as such cannot propagate transactions. Second, to be able to invoke the callback, the service cannot be configured with ConcurrencyMode.Single; otherwise, WCF will abort the call to avoid the deadlock. Typically services are configured for Client/Service or Client transaction propagation modes. Ideally, the service should be able to propagate its original calling client's transaction to the callbacks it invokes. Yet, for the service to use the client's transaction, transactionScopeRequired must be set to TRue. Since ReleaseServiceInstanceOnTransactionComplete is true by default, it requires ConcurrencyMode.Single, thus precluding the callback.

7.10.3.1. Out-of-band transactional callbacks

There are two ways of making transactional callbacks. The first is out-of-band callbacks by nonservice parties on the host side using callback references stored by the service. Such parties can easily propagate their transactions (usually in a TRansactionScope) to the callback because there is no risk of a deadlock, as shown in Example 7-26.

Example 7-26. Out-of-band callbacks

 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract {    static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>( );    public void MyMethod( )    {       IMyContractCallback callback = OperationContext.Current.                                          GetCallbackChannel<IMyContractCallback>( );       if(m_Callbacks.Contains(callback) == false)       {          m_Callbacks.Add(callback);       }    }    public static void CallClients( )    {       Action<IMyContractCallback> invoke = delegate(IMyContractCallback callback)                                            {                                               using(TransactionScope scope                                                           = new TransactionScope( ))                                               {                                                   callback.OnCallback( );                                                   scope.Complete( );                                                }                                             };       m_Callbacks.ForEach(invoke);    } } //Out-of-band callbacks: MyService.CallClients( ); 

7.10.3.2. Service transactional callback

The second option is to carefully configure the transactional service so that it is able to call back to its calling client. To that end, configure the service with ConcurrencyMode.Reentrant, set ReleaseServiceInstanceOnTransactionComplete to false, and make sure at least one operation has transactionScopeRequired set to TRue, as shown in Example 7-27.

Example 7-27. Configuring for transactional callbacks

 [ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Allowed)]    void MyMethod(...); } interface IMyContractCallback {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Allowed)]    void OnCallback( ); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,                  ConcurrencyMode = ConcurrencyMode.Reentrant,                  ReleaseServiceInstanceOnTransactionComplete = false)] class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {       Trace.WriteLine("Service ID:    " +                  Transaction.Current.TransactionInformation.DistributedIdentifier);       IMyContractCallback callback =                 OperationContext.Current.GetCallbackChannel<IMyContractCallback>( );       callback.OnCallback( );    } } 

The rationale behind this constraint is explained in the next chapter.

Given the definitions of Example 7-27 and transaction flow enabled in the binding, the following client code:

 class MyClient : IMyContractCallback {    [OperationBehavior(TransactionScopeRequired = true)]    public void OnCallback( )    {       Trace.WriteLine("OnCallback ID: " +                  Transaction.Current.TransactionInformation.DistributedIdentifier);    } } MyClient client = new MyClient( ); InstanceContext context = new InstanceContext(client); MyContractClient proxy = new MyContractClient(context); using(TransactionScope scope = new TransactionScope( )) {    proxy.MyMethod( );    Trace.WriteLine("Client ID:     " +                  Transaction.Current.TransactionInformation.DistributedIdentifier);    scope.Complete( ); } proxy.Close( ); 

yields output similar to this:

 Service ID:    23627e82-507a-45d5-933c-05e5e5a1ae78 OnCallback ID: 23627e82-507a-45d5-933c-05e5e5a1ae78 Client ID:     23627e82-507a-45d5-933c-05e5e5a1ae78 

indicating that the client transaction was propagated to the service and into the callback.

Obviously, setting ReleaseServiceInstanceOnTransactionComplete to false means WCF will not recycle the instance once the transaction completes. The best remedy for that is to prefer per-call services for transactional callbacks (as in Example 7-27) because they will be destroyed after the method returns anyway, and their state-aware programming model is independent of ReleaseServiceInstanceOnTransactionComplete.

If you are using a per-session service, you need to follow the guidelines mentioned previously on how to manage the state of a per-session service when ReleaseServiceInstanceOnTransactionComplete is false; namely, state-aware programming or utilizing volatile resource managers.




Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net