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
);
}
}
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:
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-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.
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.
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.
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.
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( );
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
indicating that the client's transaction was propagated to the service and into the callback.
18.117.188.138