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
);
}
}
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:
Use a transaction-aware duplex binding with transaction flow enabled.
Set transaction flow to mandatory on the callback operation.
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.
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.
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.
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.
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( );
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.