As hinted previously, the transactional configuration of the service is intimately related to the service instance lifecycle, and it drastically changes the programming model. All transactional services must store their state in a resource manager or managers. Those resource managers could be volatile or durable, shared between the instances or per instance, and could support multiple services, all according to your design of both the service and its resources.
With a per-call service, once the call returns, the instance is destroyed. Therefore, the resource manager used to store the state between calls must be outside the scope of the instance. Because there could be many instances of the same service type accessing the same resource manager, every operation must contain some parameters that allow the service instance to find its state in the resource manager and bind against it. The best approach is to have each operation contain some key as a parameter identifying the state. I call that parameter the state identifier. The client must provide the state identifier with every call to the per-call service. Typical state identifiers are account numbers, order numbers, and so on. For example, the client creates a new transactional order-processing object, and on every method call the client must provide the order number as a parameter, in addition to other parameters.
Example 7-15 shows a template for implementing a transactional per-call service.
Example 7-15. Implementing a transactional service
[DataContract] class Param {...} [ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod(Param stateIdentifier
); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract,IDisposable { [OperationBehavior(TransactionScopeRequired =true
)] public void MyMethod(Param stateIdentifier
) { GetState(stateIdentifier
); DoWork( ); SaveState(stateIdentifier
); } void GetState(Param stateIdentifier) {...} void DoWork( ) {...} void SaveState(Param stateIdentifier) {...} public void Dispose( ) {...} }
The MyMethod( )
signature contains a state
identifier parameter of the type Param
(a pseudotype
invented for this example), used to get the state from a resource manager with the
GetState( )
helper method. The service instance then
performs its work using the DoWork( )
helper method and
saves its state back to the resource manager using the SaveState(
)
method, specifying its identifier.
Note that not all of the service instance's state can be saved by value to the
resource manager. If the state contains references to other objects, GetState( )
should create those objects, and SaveState( )
(or Dispose(
)
) should dispose of them.
Because the service instance goes through the trouble of retrieving its state and saving it on every method call, transactional programming is natural for per-call services. The behavioral requirements for a state-aware transactional object and a per-call object are the same: both retrieve and save their state at the method boundaries. Compare Example 7-15 with Example 4-3. The only difference is that the state store used by the service in Example 7-15 should be transactional.
A far as a per-call service call is concerned, transactional programming is almost incidental. Every call on the service gets a new instance, and that call may or may not be in the same transaction as the previous call (see Figure 7-8).
Regardless of transactions, in every call the service gets its state from a resource
manager and then saves it back, so the methods are always guaranteed to operate either on
consistent state from the previous transaction or on the temporary yet well-isolated state
of the current transaction in progress. A per-call service must vote and complete its
transaction in every method call. In fact, a per-call service must always use
auto-completion (i.e., have TransactionAutoComplete
set
to its default value, true
).
From the client's perspective, the same service proxy can participate in multiple transactions or in the same transaction. For example, in the following code snippet, every call will be in a different transaction:
MyContractClient proxy = new MyContractClient( ); using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod(...); scope.Complete( ); } using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod(...); scope.Complete( ); } proxy.Close( );
Or, the client can use the same proxy multiple times in the same transaction, and even close the proxy independently of any transactions:
MyContractClient proxy = new MyContractClient( ); using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod(...); proxy.MyMethod(...); scope.Complete( ); } proxy.Close( );
The call to Dispose( )
on a per-call service has
no ambient transaction.
With a per-call service, any resource manager can be used to store the service state. For example, you might use a database, or you might use volatile resource managers accessed as static member variables, as shown in Example 7-16.
Example 7-16. Per-call service using a VRM
[ServiceContract] interface ICounterManager { [OperationContract] [TransactionFlow(...)] void Increment(string stateIdentifier); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : ICounterManager { static TransactionalDictionary<string,int> m_StateStore = new TransactionalDictionary<string,int>( ); [OperationBehavior(TransactionScopeRequired = true)] public void Increment(string stateIdentifier) { if(m_StateStore.ContainsKey(stateIdentifier) == false) { m_StateStore[stateIdentifier] = 0; } m_StateStore[stateIdentifier]++; } }
When the per-call service is the root of a transaction (that is, when it is
configured for the Client/Service transaction mode and there is no client transaction,
or when it is configured for the Service transaction mode), the transaction ends once
the service instance is deactivated. WCF completes and ends the transaction as soon as
the method returns, even before Dispose( )
is called.
When the client is the root of the transaction (or whenever the client's transaction
flows to the service and the service joins it), the transaction ends when the client's
transaction ends.
While it is possible to develop transactional sessionful services with great ease using my volatile resource managers, WCF was designed without them in mind, simply because these technologies evolved more or less concurrently. Consequently, the WCF architects did not trust developers to properly manage the state of their sessionful service in the face of transactions—something that is rather cumbersome and difficult, as you will see, if all you have at your disposal is raw .NET and WCF. The WCF architects made the extremely conservative decision to treat a sessionful transactional service as a per-call service by default in order to enforce a proper state-aware programming model. In fact, the default transaction configuration of WCF will turn any service, regardless of its instancing mode, into a per-call service. This, of course, negates the very need for a per-session service in the first place. That said, WCF does allow you to maintain the session semantic with a transactional service, using several distinct programming models. A per-session transactional service instance can be accessed by multiple transactions, or the instance can establish an affinity to a particular transaction, in which case, until it completes, only that transaction is allowed to access it. However, as you will see, unless you use volatile resource managers this support harbors a disproportional cost in programming model complexity and constraints.
The lifecycle of any transactional service is controlled by the ServiceBehavior
attribute's Boolean property, ReleaseServiceInstanceOnTransactionComplete
:
[AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute,... { public bool ReleaseServiceInstanceOnTransactionComplete {get;set;} //More members }
When ReleaseServiceInstanceOnTransactionComplete
is set to true
(the default value), it disposes of
the service instance once the instance completes the transaction. WCF uses context
deactivation (discussed in Chapter 4) to terminate the
sessionful service instance and its in-memory state, while maintaining the transport
session and the instance context.
Note that the release takes place once the instance completes the transaction, not
necessarily when the transaction really completes (which could be much later). When
ReleaseServiceInstanceOnTransactionComplete
is
true
, the instance has two ways of completing the
transaction and being released: at the method boundary if the method has TransactionAutoComplete
set to true
, or when any method that has TransactionAutoComplete
set to false
calls SetTransactionComplete( )
.
ReleaseServiceInstanceOnTransactionComplete
has
two interesting interactions with other service and operation behavior properties.
First, it cannot be set (to either true
or false
) unless at least one operation on the service has
TransactionScopeRequired
set to true
. This is validated at the service load time by the
set
accessor of the ReleaseServiceInstanceOnTransactionComplete
property.
For example, this is a valid configuration:
[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = true
)]
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{...}
[OperationBehavior(...)]
public void MyOtherMethod( )
{...}
}
What this constraint means is that even though the default of ReleaseServiceInstanceOnTransactionComplete
is true
, the following two definitions are not semantically
equivalent, because the second one will throw an exception at the service load
time:
class MyService : IMyContract
{
public void MyMethod( )
{...}
}
//Invalid definition:
[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = true
)]
class MyService : IMyContract
{
public void MyMethod( )
{...}
}
The second constraint involved in using ReleaseServiceInstanceOnTransactionComplete
relates to concurrent
multithreaded access to the service instance.
Concurrency management is the subject of the next chapter. For now, all you need to
know is that the ConcurrencyMode
property of the
ServiceBehavior
attribute controls concurrent
access to the service instance:
public enum ConcurrencyMode { Single, Reentrant, Multiple } [AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : ... { public ConcurrencyMode ConcurrencyMode {get;set;} //More members }
The default value of ConcurrencyMode
is ConcurrencyMode.Single
.
At the service load time, WCF will verify that, if TransactionScopeRequired
is set to true
for at least one operation on the service when ReleaseServiceInstanceOnTransactionComplete
is true
(by default or explicitly), the service concurrency mode is ConcurrencyMode.Single
.
For example, given this contract:
[ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod( ); [OperationContract] [TransactionFlow(...)] void MyOtherMethod( ); }
the following two definitions are equivalent and valid:
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{...}
public void MyOtherMethod( )
{...}
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,
ReleaseServiceInstanceOnTransactionComplete = true
)]
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{...}
public void MyOtherMethod( )
{...}
}
The following definition is also valid, since no method requires a transaction scope
even though ReleaseServiceInstanceOnTransactionComplete
is true
:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract { public void MyMethod( ) {...} public void MyOtherMethod( ) {...} }
In contrast, the following definition is invalid, because at least one method
requires a transaction scope, ReleaseServiceInstanceOnTransactionComplete
is true
, and yet the concurrency mode is not ConcurrencyMode.Single
:
//Invalid configuration: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple
)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired =true
)] public void MyMethod( ) {...} public void MyOtherMethod( ) {...} }
The concurrency constraint applies to all instancing modes.
The ReleaseServiceInstanceOnTransactionComplete
property can enable a transactional session interaction between the client and the
service. With its default value of true
, once the
service instance completes the transaction (either declaratively or explicitly), the
return of the method will deactivate the service instance as if it were a per-call
service.
For example, the service in Example 7-17 behaves just like a per-call service.
Example 7-17. Per-session yet per-call transactional service
[ServiceContract(SessionMode = SessionMode.Required
)]
interface IMyContract
{
[OperationContract]
[TransactionFlow(...)]
void MyMethod( );
}
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{...}
}
Every time the client calls MyMethod( )
, the
client will get a new service instance. The new client call may come in on a new
transaction as well, and the service instance has no affinity to any transaction. The
relationship between the service instances and the transactions is just as in Figure 7-8. The service needs to proactively manage
its state just as it did in Example 7-15, as
demonstrated in Example 7-18.
Example 7-18. Proactive state management by default with a per-session transactional service
[DataContract] class Param {...} [ServiceContract(SessionMode = SessionMode.Required
)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod(Param stateIdentifier); } class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod(Param stateIdentifier) {GetState
(stateIdentifier); DoWork( );SaveState
(stateIdentifier); } void GetState(Param stateIdentifier) {...} void DoWork( ) {...} void SaveState(Param stateIdentifier) {...} }
The transactional per-session service can also, of course, use VRMs, as was done in Example 7-16.
Obviously, a configuration such as that in Example 7-17 or Example 7-18 adds no value to configuring the
service as sessionful. The client must still pass a state identifier, and the service is
de facto a per-class service. To behave as a per-session service, the service can set
ReleaseServiceInstanceOnTransactionComplete
to
false
, as in Example 7-19.
Example 7-19. Per-session transactional service
[ServiceContract(SessionMode = SessionMode.Required
)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod( ); } [ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete =false
)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) {...} }
When ReleaseServiceInstanceOnTransactionComplete
is false
, the instance will not be disposed of when
transactions complete, as shown in Figure 7-9.
The interaction in Figure 7-9 might, for example, be the result of the following client code, where all calls went to the same service instance:
MyContractClient proxy = new MyContractClient( ); using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod( ); scope.Complete( ); } using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod( ); proxy.MyMethod( ); scope.Complete( ); } proxy.Close( );
When ReleaseServiceInstanceOnTransactionComplete
is false
, WCF will stay out of the way and will let
the service developer worry about managing the state of the service instance in the face
of transactions. Obviously, you have to somehow monitor transactions and roll back any
changes made to the state of the instance if a transaction aborts. The per-session
service still must equate method boundaries with transaction boundaries, because every
method may be in a different transaction, and a transaction may end between method calls
in the same session. There are two possible programming models. The first is to be
state-aware, but use the session ID as a state identifier. With this model, at the
beginning of every method the service gets its state from a resource manager using the
session ID as a key, and at the end of every method the service instance saves the state
back to the resource manager, as shown in Example 7-20.
Example 7-20. State-aware, transactional per-session service
[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false
)]
class MyService : IMyContract,IDisposable
{
readonly string m_StateIdentifier;
public MyService( )
{
InitializeState( );
m_StateIdentifier = OperationContext.Current.SessionId;
SaveState( );
}
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{
GetState( );
DoWork( );
SaveState( );
}
public void Dispose( )
{
RemoveState( );
}
//Helper methods
void InitializeState( )
{...}
void GetState( )
{
//Use m_StateIdentifier to get state
...
}
void DoWork( )
{...}
void SaveState( )
{
//Use m_StateIdentifier to save state
...
}
void RemoveState( )
{
//Use m_StateIdentifier to remove the state from the RM
...
}
}
In Example 7-20, the constructor first
initializes the state of the object and then saves the state to a resource manager, so
that any method can retrieve it. Note that the per-session object maintains the illusion
of a stateful, sessionful interaction with its client. The client does not need to pass
an explicit state identifier, but the service must be disciplined and retrieve and save
the state in every operation call. When the session ends, the service purges its state
from the resource manager in the Dispose( )
method.
The second, more modern programming model is to use volatile resource managers for the service members, as shown in Example 7-21.
Example 7-21. Using volatile resource managers to achieve a stateful per-session transactional service
[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete =false
)] class MyService : IMyContract {Transactional<string>
m_Text = new Transactional<string>("Some initial value");TransactionalArray<int>
m_Numbers = new TransactionalArray<int>(3); [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) { m_Text.Value = "This value will roll back if the transaction aborts"; //These will roll back if the transaction aborts m_Numbers[0] = 11; m_Numbers[1] = 22; m_Numbers[2] = 33; } }
Example 7-21 uses my Transactional<T>
and TransactionalArray<T>
volatile resource managers. The per-session
service can safely set ReleaseServiceInstanceOnTransactionComplete
to false
and yet freely access its members. The use of the volatile resource
managers enables a stateful programming model, and the service instance simply accesses
its state as if no transactions were involved. The volatile resource managers
auto-enlist in the transaction and isolate that transaction from all other transactions.
Any changes made to the state will commit or roll back with the transaction.
When the per-session service is the root of the transaction, the transaction ends
once the service completes the transaction, which is when the method returns. When the
client is the root of the transaction (or when a transaction flows to the service), the
transaction ends when the client's transaction ends. If the per-session service provides
an IDisposable
implementation, the Dispose( )
method will not have any transaction, regardless
of the root.
Because a per-session service can engage the same service instance in multiple
client calls, it can also sustain multiple concurrent transactions. Given the service
definition of Example 7-19, Example 7-22 shows some client code that launches
concurrent transactions on the same instance. scope2
will use a new transaction separate from that of scope1
, and yet access the same service instance in the same
session.
Example 7-22. Launching concurrent transactions
using(TransactionScope scope1 = new TransactionScope( )) { MyContractClient proxy = new MyContractClient( ); proxy.MyMethod( ); using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.RequiresNew)) { proxy.MyMethod( ); scope2.Complete( ); } proxy.MyMethod( ); proxy.Close( ); scope1.Complete( ); }
The resulting transactions of Example 7-22 are depicted in Figure 7-10.
Code such as that in Example 7-22 will almost certainly result in a transactional deadlock over the underlying resources the service accesses. The first transaction will obtain the resource lock, and the second transaction will wait to own that lock while the first transaction waits for the second to complete.
WCF offers yet another programming model for transactional per-session services,
which is completely independent of ReleaseServiceInstanceOnTransactionComplete
. This model is available for
the case when the entire session fits into a single transaction, and the service equates
session boundaries with transaction boundaries. The idea is that the service should not
complete the transaction inside the session, because that is what causes WCF to release
the service instance. To avoid completing the transaction, a per-session service can set
TransactionAutoComplete
to false
, as shown in Example 7-23.
Example 7-23. Setting TransactionAutoComplete to false
[ServiceContract(SessionMode = SessionMode.Required
)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod1( ); [OperationContract] [TransactionFlow(...)] void MyMethod2( ); [OperationContract] [TransactionFlow(...)] void MyMethod3( ); } class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete =false
)] public void MyMethod1( ) {...} [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete =false
)] public void MyMethod2( ) {...} [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete =false
)] public void MyMethod3( ) {...} }
Note that only a per-session service with a contract set to SessionMode.Required
can set TransactionAutoComplete
to false
, and
that is verified at the service load time. The problem with Example 7-23 is that the transaction the
service participates in will always abort because the service does not vote to commit it
by completing it. If the service equates sessions with transactions, the service should
vote once the session ends. For that purpose, the ServiceBehavior
attribute provides the Boolean property TransactionAutoCompleteOnSessionClose
, defined as:
[AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute,... { public bool TransactionAutoCompleteOnSessionClose {get;set;} //More members }
The default of TransactionAutoCompleteOnSessionClose
is false
. However, when set to true
, it
will auto-complete all uncompleted methods in the session. If no exceptions occurred
during the session, when TransactionAutoCompleteOnSessionClose
is true
the service will vote to commit. For example, here is how to retrofit
Example 7-23:
[ServiceBehavior(TransactionAutoCompleteOnSessionClose = true
)]
class MyService : IMyContract
{...}
Figure 7-11 shows the resulting instance and its session.
During the session, the instance can maintain and access its state in normal member variables, and there is no need for state awareness or volatile resource managers.
When joining the client's transaction and relying on auto-completion on session
close, the service must avoid lengthy processing in Dispose(
)
or, in practical terms, avoid implementing IDisposable
altogether. The reason is the race condition described here.
Recall from Chapter 4 that Dispose( )
is called asynchronously at the end of the session.
Auto-completion at session end takes place once the instance has been disposed of. If
the client has control before the instance is disposed, the transaction will abort
because the service has not yet completed it.
Note that using TransactionAutoCompleteOnSessionClose
is risky, because it is always
subjected to the transaction timeout. Sessions are by their very nature long-lived
entities, while well-designed transactions are short-lived. This programming model is
available for the case when the vote decision requires information that will be obtained
by future calls throughout the session.
Because having TransactionAutoCompleteOnSessionClose
set to true
equates the session's end with the transaction's end, it is required
that when the client's transaction is used, the client terminates the session within
that transaction:
using(TransactionScope scope = new TransactionScope( ))
{
MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( );
proxy.MyMethod( );
proxy.Close( );
scope.Complete( );
}
Failing to do so will abort the transaction. As a side effect, the client cannot
easily stack the using
statements of the transaction
scope and the proxy, because that may cause the proxy to be disposed of after the
transaction:
//This always aborts:using
(MyContractClient proxy = new MyContractClient( )) using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod( ); proxy.MyMethod( ); scope.Complete( );}
In addition, because the proxy is basically good for only one-time use, there is little point in storing the proxy in member variables.
Setting TransactionAutoComplete
to false
has a unique effect that nothing else in WCF provides:
it creates an affinity between the service instance context and the transaction, so that
only that single transaction can ever access that service instance context. Unless
context deactivation is used, this affinity is therefore to the instance as well. The
affinity is established once the first transaction accesses the service instance, and
once established it is fixed for the life of the instance (until the session ends).
Transactional affinity is available only for per-session services, because only a
per-session service can set TransactionAutoComplete
to false
. Affinity is crucial because the service is
not state-aware—it uses normal members, and it must isolate access to them from any
other transaction, in case the transaction to which it has an affinity aborts. Affinity
thus offers a crude form of transaction-based locking. With transaction affinity, code
such as that in Example 7-22 is guaranteed to
deadlock (and eventually abort due to timing out) because the second transaction is
blocked (independently of any resources the service accesses) waiting for the first
transaction to finish, while the first transaction is blocked waiting for the
second.
WCF also supports a hybrid of two of the sessionful programming models discussed
earlier, combining both a state-aware and a regular sessionful transactional per-session
service. The hybrid mode is designed to allow the service instance to maintain state in
memory until it can complete the transaction, and then recycle that state using ReleaseServiceInstanceOnTransactionComplete
as soon as
possible, instead of delaying completing the transaction until the end of the session.
Consider the service in Example 7-24, which implements
the contract from Example 7-23.
Example 7-24. Hybrid per-session service
[ServiceBehavior(TransactionAutoCompleteOnSessionClose =true
)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete =false
)] public void MyMethod1( ) {...} [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete =false
)] public void MyMethod2( ) {...} [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod3( ) {...} }
The service uses the default of ReleaseServiceInstanceOnTransactionComplete
(true
), yet it has two methods (MyMethod1(
)
and MyMethod2( )
) that do not complete
the transaction with TransactionAutoComplete
set to
false
, resulting in an affinity to a particular
transaction. The affinity isolates its members from any other transaction. The problem
now is that the transaction will always abort, because the service does not complete it.
To compensate for that, the service offers MyMethod3(
)
, which does complete the transaction. Because the service uses the default
of ReleaseServiceInstanceOnTransactionComplete
(true
), after MyMethod3(
)
is called, the transaction is completed and the instance is disposed of,
as shown in Figure 7-12. Note that MyMethod3( )
could have instead used explicit voting via
SetTransactionComplete( )
. The important thing is
that it completes the transaction. If the client does not call MyMethod3( )
, purely as a contingency, the service in Example 7-24 relies on TransactionAutoCompleteOnSessionClose
being set to true
to complete and commit the transaction.
The hybrid mode is inherently a brittle proposition. The first problem is that the service instance must complete the transaction before it times out, but since there is no telling when the client will call the completing method, you risk timing out before that. In addition, the service holds onto any locks on resource managers it may access for the duration of the session, and the longer the locks are held, the higher the likelihood is of other transactions timing out or deadlocking with this service's transaction. Finally, the service is at the mercy of the client, because the client must call the completing method to end the session. You can and should clearly document the need to call that operation at the end of the transaction:
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethod1( ); [OperationContract] [TransactionFlow(...)] void MyMethod2( ); [OperationContract] [TransactionFlow(...)] void CompleteTransaction( ); }
Both equating sessions with transactions (while relying solely on TransactionAutoCompleteOnSessionClose
) and using the hybrid
mode are potential solutions for situations when the transaction execution and
subsequent voting decision require information obtained throughout the session.
Consider, for example, the following contract used for order processing:
[ServiceContract(SessionMode = SessionMode.Required)] interface IOrderManager { [OperationContract] [TransactionFlow(...)] void SetCustomerId(int customerId); [OperationContract] [TransactionFlow(...)] void AddItem(int itemId); [OperationContract] [TransactionFlow(...)] bool ProcessOrders( ); }
The implementing service can only process the order once it has the customer ID and all of the ordered items. However, relying on transactional sessions in this way usually indicates poor design, because of the inferior throughput and scalability implications. Good transactions are inherently short while sessions are inherently long (up to 10 minutes by default), so they are inherently incompatible. The disproportional complexity of prolonging a single transaction across a session outweighs the perceived benefit of using a session. It is usually better to factor the contract so that it provides every operation with all the information it needs to complete and vote:
[ServiceContract(SessionMode = ...)] interface IOrderManager { [OperationContract] [TransactionFlow(...)] bool ProcessOrders(int customerId,int[] itemIds); }
Done this way, you can either implement the service as per-call or maintain a sessionful programming model, avoid placing operation order constraints on the client, and use any VRMs as member variables and access other transactional resources. You clearly separate the contract from its implementation, both on the client and the service side.
Recall from Chapter 4 that a durable service retrieves its
state from the configured store and then saves its state back into that store on every
operation. The state store may or may not be a transactional resource manager. If the
service is transactional, it should of course use only a transactional durable storage and
enlist it in each operation's transaction. That way, if a transaction aborts, the state
store will be rolled back to its pre-transaction state. However, WCF does not know whether
a service is designed to propagate its transactions to the state store, and by default it
will not enlist the storage in the transaction even if the storage is a transactional
resource manager, such as SQL Server 2005/2008. To instruct WCF to propagate the
transaction and enlist the underlying storage, set the SaveStateInOperationTransaction
property of the DurableService
attribute to true
:
public sealed class DurableServiceAttribute : ... { public bool SaveStateInOperationTransaction {get;set;} }
SaveStateInOperationTransaction
defaults to
false
, which means the state storage will not
participate in the transaction. It is therefore important to always set SaveStateInOperationTransaction
to true
to ensure consistent state management in the presence of transactions.
Since only a transactional service could benefit from having SaveStateInOperationTransaction
set to true
, if it is true
then WCF will insist
that all operations on the service either have TransactionScopeRequired
set to true
or
have mandatory transaction flow. If the operation is configured with TransactionScopeRequired
set to true
, the ambient transaction of the operation will be the one used to enlist
the storage. If the operation is configured for mandatory transaction flow, the client's
transaction will be used to enlist the storage (regardless of whether the operation does
or does not have an ambient transaction).
As explained in Chapter 4, the DurableService
behavior attribute enforces strict management of the
instance ID passed over the context binding. The first operation to start the workflow
will have no instance ID, in which case, WCF will create a new instance ID, use it to
save the newly created instance state to the storage, and then send the instance ID back
to the client. From that point on, until the end of the workflow, the client must pass
the same instance ID to the service. If the client provides an instance ID that is not
present in the storage, WCF will throw an exception. This presents a potential pitfall
with transactional durable services: suppose the client starts a workflow and propagates
its transaction to the service. The first operation creates the instance ID, executes
successfully, and stores the state in the storage. However, what would happen if the
transaction were then to abort, due to some other party (such as the client or another
service involved in the transaction) voting to abort? The state storage would roll back
the changes made to it, including the newly created instance state and the corresponding
ID. The next call coming from the client will present the same ID created by the first
call, except now the state storage will not have any record of that ID, so WCF will
reject the call, throw an exception, and prevent any other call to the service with that
ID from ever executing.
To avoid this pitfall, you need to add to the service contract an explicit first
operation whose sole purpose is to guarantee that the first call successfully commits
the instance ID to the state storage. For example, in the case of a calculator service,
this would be your PowerOn( )
operation. You should
explicitly block the client's transaction (by using the default value of TransactionFlowOption.NotAllowed
), and avoid placing any
code in that method body, thus precluding anything that could go wrong from aborting the
transaction. You can enforce having the client call the initiating operation first using
demarcating operations (discussed in Chapter 4).
A similar pitfall exists at the end of the workflow. By setting the CompletesInstance
property of the DurableOperation
attribute to true
, you
indicate to WCF that the workflow has ended and that WCF should purge the instance state
from the storage. However, if the client's transaction aborts after the last operation
in the service has executed successfully, the storage will roll back and keep the
orphaned state indefinitely. To avoid bloating the state storage with zombie instances
(the product of aborted transactions of the completing instance operations), you need to
add to the service contract an explicit operation whose sole purpose is to complete the
instance and to commit successfully, irrespective of whether the client's transaction
commits. For example, in the case of a calculator service, this would be your PowerOff( )
operation. Again, block any client transaction
from propagating to the service, and avoid placing any code in the completing
method.
Example 7-25 shows a template for defining and implementing a transactional durable service, adhering to these guidelines.
Example 7-25. Transactional durable service
[ServiceContract] interface IMyContract { [OperationContract] void SaveState( ); [OperationContract(IsInitiating = false)] void ClearState( ); [OperationContract(IsInitiating = false)] [TransactionFlow(...)] void MyMethod1( ); [OperationContract(IsInitiating = false)] [TransactionFlow(...)] void MyMethod2( ); } [Serializable] [DurableService(SaveStateInOperationTransaction = true
)] class MyService: IMyContract { [OperationBehavior(TransactionScopeRequired = true
)] public void SaveState( ){}
[DurableOperation(CompletesInstance = true
)] [OperationBehavior(TransactionScopeRequired = true
)] public void ClearState( ){}
[OperationBehavior(TransactionScopeRequired = true)] public void MyMethod1( ) {...} [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod2( ) {...} }
As far as the DurableService
attribute is
concerned, the word Durable
in its name is a misnomer,
since it does not necessarily indicate a durable behavior. All it means is that WCF will
automatically deserialize the service state from a configured storage and then serialize
it back again on every operation. Similarly, the persistence
provider
behavior (see Chapter 4) does not
necessarily mean persistence, since any provider that derives from the prescribed abstract
provider class will comply with WCF's expectation of the behavior.
The fact that the WCF durable service infrastructure is, in reality, a serialization infrastructure enabled me to leverage it into yet another technique for managing service state in the face of transactions, while relying underneath on a volatile resource manager, without having the service instance do anything about it. This further streamlines the transactional programming model of WCF and yields the benefit of the superior programming model of transactions for mere objects.
The first step was to define two transactional in-memory provider factories:
public abstract class MemoryProviderFactory : PersistenceProviderFactory {...} public class TransactionalMemoryProviderFactory : MemoryProviderFactory {...} public class TransactionalInstanceProviderFactory : MemoryProviderFactory {...}
The TransactionalMemoryProviderFactory
uses my
TransactionalDictionary<ID,T>
to store the
service instances.
Unrelated to this section and transactions, you can configure the service to use the
TransactionalMemoryProviderFactory
with or without
transactions by simply listing it in the persistence
providers
section of the service behaviors:
<behavior name = "TransactionalMemory"> <persistenceProvider type = "ServiceModelEx. TransactionalMemoryProviderFactory, ServiceModelEx" /> </behavior>
This will enable you to store the instances in memory, instead of in a file or SQL Server database. This is useful for quick testing and for stress testing, since it avoids the inherent I/O latency of a durable persistent storage.
The in-memory dictionary is shared among all clients and transport sessions, and as
long as the host is running, TransactionalMemoryProviderFactory
allows clients to connect and disconnect
from the service. When using TransactionalMemoryProviderFactory
you should designate a completing
operation that removes the instance state from the store as discussed in Chapter 4, using the CompletesInstance
property of the DurableOperation
attribute.
TransactionalInstanceProviderFactory
, on the other
hand, matches each transport session with a dedicated instance of Transactional<T>
. There is no need to call any completing operation
since the service state will be cleaned up with garbage collection after the session is
closed.
Next, I defined the TransactionalBehaviorAttribute
,
shown in Example 7-26.
Example 7-26. The TransactionalBehavior attribute
[AttributeUsage(AttributeTargets.Class)] public class TransactionalBehaviorAttribute : Attribute,IServiceBehavior { public bool TransactionRequiredAllOperations {get;set;} public bool AutoCompleteInstance {get;set;} public TransactionalBehaviorAttribute( ) { TransactionRequiredAllOperations = true; AutoCompleteInstance = true; } void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase host) { DurableServiceAttribute durable = new DurableServiceAttribute( ); durable.SaveStateInOperationTransaction = true; description.Behaviors.Add(durable); PersistenceProviderFactory factory; if(AutoCompleteInstance) { factory = new TransactionalInstanceProviderFactory( ); } else { factory = new TransactionalMemoryProviderFactory( ); } PersistenceProviderBehavior persistenceBehavior = new PersistenceProviderBehavior(factory); description.Behaviors.Add(persistenceBehavior); if(TransactionRequiredAllOperations) { foreach(ServiceEndpoint endpoint in description.Endpoints) { foreach(OperationDescription operation in endpoint.Contract.Operations) { OperationBehaviorAttribute operationBehavior = operation.Behaviors.Find<OperationBehaviorAttribute>( ); operationBehavior.TransactionScopeRequired = true; } } } } void IServiceBehavior.AddBindingParameters(...) {} void IServiceBehavior.ApplyDispatchBehavior(...) {} }
TransactionalBehavior
is a service behavior
attribute. It always performs these configurations for the service. First, it injects into
the service description a DurableService
attribute with
SaveStateInOperationTransaction
set to true
. Second, it adds the use of either TransactionalMemoryProviderFactory
or TransactionalInstanceProviderFactory
for the persistent behavior according to
the value of the AutoCompleteInstance
property. If
AutoCompleteInstance
is set to true
(the default) then TransactionalBehavior
will use TransactionalInstanceProviderFactory
. Finally, TransactionalBehavior
provides the TransactionRequiredAllOperations
property. When the property is set to
true
(the default) TransactionalBehavior
will set TransactionScopeRequired
to true
on all
the service operation behaviors, thus providing all operations with an ambient
transaction. When it is explicitly set to false
, the
service developer can choose which operations will be transactional.
As a result, using the attribute like so:
[Serializable] [TransactionalBehavior] class MyService : IMyContract { public void MyMethod( ) {...} }
is equivalent to this service declaration and configuration:
[Serializable] [DurableService(SaveStateInOperationTransaction = true)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) {...} } <services> <service name = "MyService" behaviorConfiguration = "TransactionalBehavior"> ... </service> </services> <behaviors> <serviceBehaviors> <behavior name = "TransactionalBehavior"> <persistenceProvider type = "ServiceModelEx.TransactionalInstanceProviderFactory, ServiceModelEx" /> </behavior> </serviceBehaviors> </behaviors>
When using the TransactionalBehavior
attribute with
the default values, the client need not manage or interact in any way with the instance ID
as shown in Chapter 4. All the client needs to do is use the
proxy over one of the context bindings, and let the binding manage the instance ID. For
example, for this service definition:
[ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed)] void IncrementCounter( ); } [Serializable] [TransactionalBehavior] class MyService : IMyContract { int m_Counter = 0; public void IncrementCounter( ) { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } }
the following client code:
MyContractClient proxy = new MyContractClient( ); using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); scope.Complete( ); } //This transaction will abort since the scope is not completed using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); } using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); scope.Complete( ); } proxy.Close( );
yields this output:
Counter = 1
Counter = 2
Counter = 2
Note that the service was interacting with a normal integer as its member variable.
The TransactionalBehavior
attribute substantially
simplifies transactional programming and is a fundamental step toward the future, where
memory itself will be transactional and it will be possible for every object to be
transactional (for more on my vision for the future of the platform, please see Appendix A). TransactionalBehavior
maintains the programming model of conventional,
plain .NET, yet it provides the full benefits of transactions.
To allow the efficient use of TransactionalBehavior
even in the most intimate execution scopes,
ServiceModelEx contains the NetNamedPipeContextBinding
class. As the binding's name implies, it is the
IPC binding plus the context protocol (required by the DurableService
attribute). Appendix B walks
through implementing the NetNamedPipeContextBinding
class.
Supporting TransactionalBehavior
over IPC was
my main motivation for developing the NetNamedPipeContextBinding
.
To make the programming model of TransactionalBehavior
even more accessible, the InProcFactory
class from Chapter 1 actually uses
NetNamedPipeContextBinding
instead of the built-in
NetNamedPipeBinding
. InProcFactory
also flows transactions over the binding. This enables the
programming model of Example 7-27, without
ever resorting to host management or client or service config files.
Example 7-27. Combining TransactionalBehavior with the InProcFactory
[ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed)] void IncrementCounter( ); } [Serializable][TransactionalBehavior]
class MyService : IMyContract { int m_Counter = 0; public void IncrementCounter( ) { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } } //Client-code IMyContract proxy =InProcFactory.CreateInstance
<MyService,IMyContract>( ); using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); scope.Complete( ); } //This transaction will abort since the scope is not completed using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); } using(TransactionScope scope = new TransactionScope( )) { proxy.IncrementCounter( ); scope.Complete( ); } InProcFactory.CloseProxy(proxy); //Traces: Counter = 1 Counter = 2 Counter =2
By default, a transactional singleton behaves like a per-call service. The reason is
that by default ReleaseServiceInstanceOnTransactionComplete
is set to true
, so after the singleton auto-completes a transaction, WCF
disposes of the singleton, in the interest of state management and consistency. This, in
turn, implies that the singleton must be state-aware and must proactively manage its state
in every method call, in and out of a resource manager. The big difference compared to a
per-call service is that WCF will enforce the semantic of the single instance, so at any
point in time there will be at most a single instance running. WCF uses concurrency
management and instance deactivation to enforce this rule. Recall that when ReleaseServiceInstanceOnTransactionComplete
is true
, the concurrency mode must be ConcurrencyMode.Single
to disallow concurrent calls. WCF keeps the singleton
context and merely deactivates the instance hosted in the context, as discussed in Chapter 4. What this means is that even though the singleton needs
to be state-aware, it does not need the client to provide an explicit state identifier in
every call. The singleton can use any type-level constant to identify its state in the
state resource manager, as shown in Example 7-28.
Example 7-28. State-aware singleton
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : IMyContract { readonly static string m_StateIdentifier = typeof(MySingleton).GUID.ToString( ); [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) { GetState( ); DoWork( ); SaveState( ); } //Helper methods void GetState( ) { //Use m_StateIdentifier to get state } void DoWork( ) {}public
void SaveState( ) { //Use m_StateIdentifier to save state }public
void RemoveState( ) { //Use m_StateIdentifier to remove the state from the resource manager } } //Hosting code MySingleton singleton = new MySingleton( ); singleton.SaveState( )
; //Create the initial state in the resource manager ServiceHost host = new ServiceHost(singleton); host.Open( ); /* Some blocking calls */ host.Close( ); singleton.RemoveState( )
;
In this example, the singleton uses the unique GUID associated with every type as a
state identifier. At the beginning of every method call the singleton reads its state, and
at the end of each method call it saves the state back to the resource manager. However,
the first call on the first instance must also be able to bind to the state, so you must
prime the resource manager with the state before the first call ever arrives. To that end,
before launching the host, you need to create the singleton, save its state to the
resource manager, and then provide the singleton instance to ServiceHost
(as explained in Chapter 4). After
the host shuts down, make sure to remove the singleton state from the resource manager, as
shown in Example 7-28. Note that you cannot create the initial
state in the singleton constructor, because the constructor will be called for each
operation on the singleton and will override the previous saved state.
While a state-aware singleton is certainly possible (as demonstrated in Example 7-28), the overall complexity involved makes it a technique to avoid. It is better to use a stateful transactional singleton, as presented next.
By setting ReleaseServiceInstanceOnTransactionComplete
to false
, you regain the singleton semantic. The singleton will be created
just once, when the host is launched, and the same single instance will be shared across
all clients and transactions. The problem is, of course, how to manage the state of the
singleton. The singleton has to have state; otherwise, there is no point in using a
singleton in the first place. The best solution (as before, with the stateful
per-session service) is to use volatile resource managers as member variables, as shown
in Example 7-29.
Example 7-29. Achieving a stateful singleton transactional service
////////////////// Service Side ////////////////////////////////////// [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single
,ReleaseServiceInstanceOnTransactionComplete = false
)] class MySingleton : IMyContract {Transactional<int>
m_Counter = new Transactional<int>( ); [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) { m_Counter.Value++; Trace.WriteLine("Counter: " + m_Counter.Value); } } ////////////////// Client Side ////////////////////////////////////// using(TransactionScope scope1 = new TransactionScope( )) { MyContractClient proxy =new
MyContractClient( ); proxy.MyMethod( ); proxy.Close( ); scope1.Complete( ); } using(TransactionScope scope2 = new TransactionScope( )) { MyContractClient proxy =new
MyContractClient( ); proxy.MyMethod( ); proxy.Close( ); } using(TransactionScope scope3 = new TransactionScope( )) { MyContractClient proxy =new
MyContractClient( ); proxy.MyMethod( ); proxy.Close( ); scope3.Complete( ); } ////////////////// Output ////////////////////////////////////// Counter: 1 Counter: 2 Counter: 2
In Example 7-29, a client creates three
transactional scopes, each with its own new proxy to the singleton. In each call, the
singleton increments a counter it maintains as a Transactional<int>
volatile resource manager. scope1
completes the transaction and commits the new value
of the counter (1). In scope2
, the client calls the
singleton and temporarily increments the counter to 2. However, scope2
does not complete its transaction. The volatile resource manager
therefore rejects the increment and reverts to its previous value of 1. The call in
scope3
then increments the counter again from 1 to
2, as shown in the trace output.
Note that when setting ReleaseServiceInstanceOnTransactionComplete
, the singleton must have at
least one method with TransactionScopeRequired
set to
true
.
In addition, the singleton must have TransactionAutoComplete
set to true
on
every method, which of course precludes any transactional affinity and allows concurrent
transactions. All calls and all transactions are routed to the same instance. For
example, the following client code will result in the transaction diagram shown in Figure 7-13:
using (MyContractClient proxy = new MyContractClient( )) using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod( ); scope.Complete( ); } using(MyContractClient proxy = new MyContractClient( )) using(TransactionScope scope = new TransactionScope( )) { proxy.MyMethod( ); proxy.MyMethod( ); scope.Complete( ); }
To summarize the topic of instance management modes and transactions, Table 7-3 lists the possible configurations discussed so far and their resulting effects. Other combinations may technically be allowed, but I've omitted them because they are either nonsensical or plainly disallowed by WCF.
Table 7-3. Possible instancing modes, configurations, and transactions
Configured instancing mode |
Auto- complete |
Release on complete |
Complete on session close |
Resulting instancing mode |
State mgmt. |
Trans. affinity |
---|---|---|---|---|---|---|
Per-call |
True |
True/False |
True/False |
Per-call |
State-aware |
Call |
Session |
True |
True |
True/False |
Per-call |
State-aware |
Call |
Session |
True |
False |
True/False |
Session |
VRM members |
Call |
Session |
False |
True/False |
True |
Session |
Stateful |
Instance context |
Session |
Hybrid |
True |
True/False |
Hybrid |
Hybrid |
Instance context |
Durable service |
True |
True/False |
True/False |
Per-call |
Stateful |
Call |
Singleton |
True |
True |
True/False |
Per-call |
State-aware |
Call |
Singleton |
True |
False |
True/False |
Singleton |
VRM members |
Call |
With so many options, which mode should you choose? I find that the complexity of an
explicit state-aware programming model with sessionful and singleton services outweighs
any potential benefits, and this is certainly the case with the hybrid mode as well.
Equating sessions with transactions is often impractical and indicates a bad design. For
both sessionful and singleton services, I prefer the simplicity and elegance of volatile
resource managers as member variables. You can also use a durable service on top of a
transactional durable storage or the TransactionalBehavior
attribute.
Table 7-4 lists these recommended configurations. None of the recommended options relies on transactional affinity or auto-completion on session close, but they all use auto-completion.
Table 7-4. Recommended instancing modes, configurations, and transactions
Configured instancing mode |
Release on complete |
Resulting instancing mode |
State management |
---|---|---|---|
Per-call |
True/False |
Per-call |
State-aware |
Session |
False |
Session |
VRM members |
Durable service |
True/False |
Per-call |
Stateful |
Singleton |
False |
Singleton |
VRM members |
3.138.35.193