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 Binding
Requirement
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 Transaction
Scope
) 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
This indicates that the client’s transaction was propagated to the service and into the callback.
3.22.27.45