Callbacks

Callback contracts, just like service contracts, can propagate the service transaction to the callback client. To enable this 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 whether to require a transaction scope and auto-completion:

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

Callback Transaction Modes

The callback client can have four modes of configuration: Service, Service/Callback, Callback, and None. These are analogous to the service transaction modes, except the service now plays the client role and the callback plays the service role. For example, to configure the callback for the Service transaction mode (that is, to always use the service’s 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-30 shows a callback client configured for the Service transaction mode.

Example 7-30. Configuring the callback for the Service transaction mode

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-aware binding with transaction flow enabled.

When you configure the callback for the Service/Callback transaction propagation mode, WCF does not enforce enabling of transaction flow in the binding. You can use my BindingRequirement attribute to enforce this:

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

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

public interface IEndpointBehavior
{
   void AddBindingParameters(ServiceEndpoint endpoint,
                             BindingParameterCollection bindingParameters);
   void ApplyClientBehavior(ServiceEndpoint endpoint,
                            ClientRuntime clientRuntime);
   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.

Isolation and timeouts

As with 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 the same reasoning can be used to choose a particular value.

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 result in a vote to abort the transaction, and without an error WCF will vote to commit the transaction, as is the case in Example 7-30. However, unlike with a service instance, the callback instance lifecycle 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. Voting can then be done 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.

Using Transactional Callbacks

While WCF provides the infrastructure for propagating the service’s 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 to its calling client, the service cannot be configured with ConcurrencyMode.Single; otherwise, WCF will abort the call to avoid a deadlock. Typically, services are configured for either the Client/Service or the Client transaction propagation mode. Ideally, a service should be able to propagate its original calling client’s transaction to all callbacks it invokes, even if the callback is to the calling client. 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 to the calling client.

Out-of-band transactional callbacks

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

Example 7-31. 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 = (callback)=>
                                           {
                                              using(TransactionScope scope =
                                                           new TransactionScope())
                                              {
                                                  callback.OnCallback();
                                                  scope.Complete();
                                              }
                                            };
      m_Callbacks.ForEach(invoke);
   }
}
//Out-of-band callbacks:
MyService.CallClients();

Service transactional callbacks

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-32.

Example 7-32. 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-32, if transaction flow is enabled in the binding, the following client code:

class MyCallback : IMyContractCallback
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void OnCallback()
   {
      Trace.WriteLine("OnCallback ID: " +
                Transaction.Current.TransactionInformation.DistributedIdentifier);
   }
}
MyCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);
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

This indicates that the client’s transaction was propagated to the service and into the callback.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.27.45