For services, WCF offers a simple and elegant declarative programming model. This model is, however, unavailable for non-service code called by services and for non-service WCF clients.
By default, the service class and all its operations have no ambient transaction. This is the case even when the client's transaction is propagated to the service. Consider the following service:
[ServiceContract]
interface IMyContract
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
void MyMethod( );
}
class MyService : IMyContract
{
public void MyMethod( )
{
Transaction transaction = Transaction.Current;
Debug.Assert(transaction == null
);
}
}
The ambient transaction of the service will be null
, even though the mandatory transaction flow guarantees the client's
transaction propagation. To have an ambient transaction, for each contract method the
service must indicate that it wants WCF to scope the body of the method with a
transaction. For that purpose, WCF provides the TransactionScopeRequired
property of the OperationBehaviorAttribute
:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute,... { public bool TransactionScopeRequired {get;set;} //More members }
The default value of TransactionScopeRequired
is
false
, which is why by default the service has no
ambient transaction. Setting TransactionScopeRequired
to true
provides the operation with an ambient
transaction:
class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true
)] public void MyMethod( ) { Transaction transaction = Transaction.Current; Debug.Assert(transaction != null
); } }
If the client's transaction is propagated to the service, WCF will set the client's transaction as the operation's ambient transaction. If not, WCF will create a new transaction for that operation and set the new transaction as the ambient transaction.
The service class constructor does not have a transaction: it can never participate in the client's transaction, and you cannot ask WCF to scope it with a transaction. Unless you manually create a new ambient transaction (as shown later), do not perform transactional work in the service constructor and never expect to participate in the transaction of the client that created the instance inside the constructor.
Figure 7-6 demonstrates which transaction a WCF service uses as a product of the binding configuration, the contract operation, and the local operation behavior attribute.
In the figure, a nontransactional client calls Service 1. The operation contract is
configured with TransactionFlowOption.Allowed
. Even
though transaction flow is enabled in the binding, since the client has no transaction, no
transaction is propagated. The operation behavior on Service 1 is configured to require a
transaction scope. As a result, WCF creates a new transaction for Service 1 (Transaction A
in Figure 7-6). Service 1 then calls three
other services, each configured differently. The binding used for Service 2 has
transaction flow enabled, and the operation contract mandates the flow of the client
transaction. Since the operation behavior is configured to require transaction scope, WCF
sets Transaction A as the ambient transaction for Service 2. The call to Service 3 has the
binding and the operation contract disallow transaction flow. However, since Service 3 has
its operation behavior require a transaction scope, WCF creates a new transaction for
Service 3 (Transaction B) and sets it as the ambient transaction for Service 3. Similar to
Service 3, the call to Service 4 has the binding and the operation contract disallow
transaction flow. But since Service 4 does not require a transaction scope, it has no
ambient transaction.
Which transaction the service uses is determined by the flow property of the binding (two values), the flow option in the operation contract (three values), and the value of the transaction scope property in the operation behavior (two values). There are therefore 12 possible configuration settings. Out of these 12, 4 are inconsistent and are precluded by WCF (such as flow disabled in the binding, yet mandatory flow in the operation contract) or are just plain impractical or inconsistent. Table 7-2 lists the remaining eight permutations.[4]
Table 7-2. Transaction modes as the product of binding, contract, and behavior
Binding transaction flow |
TransactionFlowOption |
TransactionScopeRequired |
Transaction mode |
---|---|---|---|
False |
Allowed |
False |
None |
False |
Allowed |
True |
Service |
False |
NotAllowed |
False |
None |
False |
NotAllowed |
True |
Service |
True |
Allowed |
False |
None |
True |
Allowed |
True |
Client/Service |
True |
Mandatory |
False |
None |
True |
Mandatory |
True |
Client |
Those eight permutations actually result in only four transaction propagation modes. I call these four modes Client/Service, Client, Service, and None. Table 7-2 also shows in bold font the recommended way to configure each mode. Each of these modes has its place in designing your application, and understanding how to select the correct mode is not only a key to sound design, but also greatly simplifies thinking about and configuring transaction support.
The Client/Service mode, as its name implies, ensures the service uses the client's transaction if possible, or a service-side transaction when the client does not have a transaction. To configure this mode:
Select a transactional binding and enable transaction flow by setting TransactionFlow
to true
.
Set the transaction flow option in the operation contract to TransactionFlowOption.Allowed
.
Set the TransactionScopeRequired
property of
the operation behavior to true
.
The Client/Service mode is the most decoupled configuration, because in this mode the service minimizes its assumptions about what the client is doing. The service will join the client's transaction if the client has a transaction to flow, which is always good for overall system consistency: if the service has a transaction separate from that of the client, one of those transactions could commit while the other aborts, leaving the system in an inconsistent state. However, if the service joins the client's transaction, all the work done by the client and the service (and potentially other services the client calls) will be committed or aborted as one atomic operation. If the client does not have a transaction, the service still requires the protection of a transaction, so this mode provides a contingent transaction to the service by making it the root of a new transaction.
Example 7-2 shows a service configured for the Client/Service transaction mode.
Example 7-2. Configuring for the Client/Service transaction mode
[ServiceContract]
interface IMyContract
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
void MyMethod( );
}
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{
Transaction transaction = Transaction.Current;
Debug.Assert(transaction != null
);
}
}
Note in Example 7-2 that the service can assert that it always has a transaction, but it cannot assume or assert whether it is the client's transaction or a locally created one. The Client/Service mode is applicable when the service can be used standalone or as part of a bigger transaction. When you select this mode, you should be mindful of potential deadlocks—if the resulting transaction is a service-side transaction, it may deadlock with other transactions trying to access the same resources, because the resources will isolate access per transaction and the service-side transaction will be a new transaction. When you use the Client/Service mode, the service may or may not be the root of the transaction, and the service must not behave differently when it is the root and when it is joining the client's transaction.
The Client/Service mode requires the use of a transaction-aware binding with
transaction flow enabled, but this is not enforced by WCF at service load time. To
tighten this loose screw, you can use my BindingRequirementAttribute
:
[AttributeUsage(AttributeTargets.Class)] public class BindingRequirementAttribute : Attribute,IServiceBehavior { public bool TransactionFlowEnabled //Default is false {get;set;} //More members }
You apply the attribute directly on the service class. The default of TransactionFlowEnabled
is false
. However, when you set it to true
,
per endpoint, if the contract of the endpoint has at least one operation with the
TransactionFlow
attribute configured with TransactionFlowOption.Allowed
, the BindingRequirement
attribute will enforce that the endpoint uses a
transaction-aware binding with the TransactionFlowEnabled
property set to true
:
[ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed
)] void MyMethod( ); } [BindingRequirement(TransactionFlowEnabled = true
)] class MyService : IMyContract {...}
To enforce the binding requirement, in the case of a mismatch an InvalidOperationException
is thrown when the host is
launched. Example 7-3 shows a somewhat
simplified implementation of the BindingRequirement
attribute.
Example 7-3. BindingRequirement attribute implementation
[AttributeUsage(AttributeTargets.Class)] public class BindingRequirementAttribute : Attribute,IServiceBehavior { public bool TransactionFlowEnabled {get;set;} void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase host) { if(TransactionFlowEnabled == false) { return; } foreach(ServiceEndpoint endpoint in description.Endpoints) { foreach(OperationDescription operation in endpoint.Contract.Operations) { TransactionFlowAttribute attribute = operation.Behaviors.Find<TransactionFlowAttribute>( ); if(attribute != null) { if(attribute.Transactions == TransactionFlowOption.Allowed) { if(endpoint.Binding is NetTcpBinding) { NetTcpBinding tcpBinding = endpoint.Binding as NetTcpBinding; if(tcpBinding.TransactionFlow == false) { throw new InvalidOperationException(...); } continue; } ... //Similar checks for the rest of the transaction-aware //bindings throw new InvalidOperationException(...); } } } } } void IServiceBehavior.AddBindingParameters(...) {} void IServiceBehavior.ApplyDispatchBehavior(...) {} }
The BindingRequirementAttribute
class is a
service behavior, so it supports the IServiceBehavior
interface introduced in Chapter 6. The Validate(
)
method of IServiceBehavior
is called
during the host launch time, enabling you to abort the service load sequence. The first
thing Validate( )
does is to check whether the
TransactionFlowEnabled
property is set to false
. If so, Validate( )
does nothing and returns. If TransactionFlowEnabled
is true
, Validate(
)
iterates over the collection of service endpoints available in the service
description. For each endpoint, it obtains the collection of operations, and for each
operation, it accesses its collection of operation behaviors. All operation behaviors
implement the IOperationBehavior
interface, including
the TransactionFlowAttribute
. If the TransactionFlowAttribute
behavior is found, Validate( )
checks whether the attribute is configured with
TransactionFlowOption.Allowed
. If so, Validate( )
checks the binding. For each transaction-aware
binding, it verifies that the TransactionFlow
property is set to true
, and if not, it throws an
InvalidOperationException
. Validate( )
also throws an InvalidOperationException
if a nontransactional binding is used for the
endpoint.
The technique shown in Example 7-3
for implementing the BindingRequirement
attribute
is a general-purpose technique you can use to enforce any binding requirement or
custom validation rule. For example, the BindingRequirement
attribute has another property, called WCFOnly
, that enforces the use of WCF-to-WCF bindings
only, and a ReliabilityRequired
property that
insists on the use of a reliable binding with reliability enabled:
[AttributeUsage(AttributeTargets.Class)] public class BindingRequirementAttribute : Attribute,IServiceBehavior { public bool ReliabilityRequired {get;set;} public bool TransactionFlowEnabled {get;set;} public bool WCFOnly {get;set;} }
The Client mode ensures the service uses only the client's transaction. To configure this mode:
Select a transactional binding and enable transaction flow by setting TransactionFlow
to true
.
Set the transaction flow option in the operation contract to TransactionFlowOption.Mandatory
.
Set the TransactionScopeRequired
property of
the operation behavior to true
.
You should select the Client transaction mode when the service must use its client's transactions and can never be used standalone, by design. The main motivation for this is to maximize overall system consistency, since the work of the client and the service is always treated as one atomic operation. Another motivation is that by having the service share the client's transaction you reduce the potential for a deadlock, because all resources accessed will enlist in the same transaction. This means no other transactions will compete for access to the same resources and underlying locks.
Example 7-4 shows a service configured for the Client transaction mode.
Example 7-4. Configuring for the Client transaction mode
[ServiceContract] interface IMyContract { [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory
)] void MyMethod( ); } class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) { Transaction transaction = Transaction.Current; Debug.Assert(transaction.TransactionInformation.DistributedIdentifier != Guid.Empty
); } }
Note in Example 7-4 that MyMethod( )
asserts the fact that the ambient transaction is
a distributed one, meaning it originated with the client.
The Service mode ensures that the service always has a transaction, separate from any transaction its clients may or may not have. The service will always be the root of a new transaction. To configure this mode:
You can select any binding. If you select a transaction-aware binding, leave its
default value for the TransactionFlow
property,
or explicitly set it to false
.
Do not apply the TransactionFlow
attribute,
or configure it with TransactionFlowOption.NotAllowed
.
Set the TransactionScopeRequired
property of
the operation behavior to true
.
You should select the Service transaction mode when the service needs to perform transactional work outside the scope of the client's transaction (e.g., when you want to perform some logging or audit operations, or when you want to publish events to subscribers regardless of whether the client's transaction commits or aborts). As an example, consider a logbook service that performs error logging into a database. When an error occurs on the client side, the client will use the logbook service to log it or some other entries. But after it's logged, the error on the client side aborts the client's transaction. If the service were to use the client's transaction, once the client's transaction aborts, the logged error would be discarded from the database, and you would have no trace of it (defeating the purpose of the logging in the first place). Configuring the service to have its own transaction, on the other hand, ensures that the log of the error is committed even when the client's transaction aborts.
The downside, of course, is the potential for jeopardizing the consistency of the system, because the service's transaction could abort while the client's commits. To avoid this pitfall, if the service-side transaction aborts, WCF throws an exception on the calling client side, even if the client was not using transactions or if the binding did not propagate any transaction. I therefore recommend that you only choose the Service mode if you have a supporting heuristic. The heuristic must be that the service's transaction is much more likely to succeed and commit than the client's transaction. In the example of the logging service, this is often the case, because once deterministic logging is in place it will usually work (unlike business transactions, which may fail for a variety of reasons).
In general, you should be extremely careful when using the Service transaction mode, and verify that the two transactions (the client's and the service's) do not jeopardize consistency if one aborts and the other commits. Logging and auditing services are the classic candidates for this mode.
Example 7-5 shows a service configured for the Service transaction mode.
Example 7-5. Configuring for the Service transaction mode
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod( );
}
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{
Transaction transaction = Transaction.Current;
Debug.Assert(transaction.TransactionInformation.
DistributedIdentifier == Guid.Empty
);
}
}
Note in Example 7-5 that the service can assert that it actually has a local transaction.
If the None transaction mode is configured, the service never has a transaction. To configure this mode:
You can select any binding. If you select a transaction-aware binding, leave its
default value for the TransactionFlow
property,
or explicitly set it to false
.
Do not apply the TransactionFlow
attribute,
or configure it with TransactionFlowOption.NotAllowed
.
You do not need to set the TransactionScopeRequired
property of the operation behavior, but if you
do, you should set it to false
.
The None transaction mode is useful when the operations performed by the service are nice to have but not essential, and should not abort the client's transaction if they fail. For example, a service that prints a receipt for a money transfer should not be able to abort the client transaction if it fails because the printer is out of paper. Another example where the None mode is useful is when you want to provide some custom behavior, and you need to perform your own programmatic transaction support or manually enlist resources (for example, when calling legacy code, as in Example 7-1). Obviously, there is danger when using the None mode because it can jeopardize the system's consistency. Say the calling client has a transaction and it calls a service configured for the None transaction mode. If the client aborts its transaction, changes made to the system state by the service will not roll back. Another pitfall of this mode is that if a service configured for the None mode calls another service configured for the Client mode, the call will fail because the calling service has no transaction to propagate.
Example 7-6 shows a service configured for the None transaction mode.
Example 7-6. Configuring for the None transaction mode
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod( );
}
class MyService : IMyContract
{
public void MyMethod( )
{
Transaction transaction = Transaction.Current;
Debug.Assert(transaction == null
);
}
}
Note that the service in Example 7-6 can assert that it has no ambient transaction.
The None mode allows you to have a nontransactional service be called by a transactional client. As stated previously, the None mode is typically used for services that perform nice-to-have operations. The problem with this usage is that any exception thrown by the None service will abort the calling client's transaction, which should be avoided for mere nice-to-have operations. The solution is to have the client catch all exceptions from the None service to avoid contaminating the client's transaction. For example, here's how a client could call the service from Example 7-6:
MyContractClient proxy = new MyContractClient( ); try { proxy.MyMethod( ); proxy.Close( ); }catch
{}
You need to encase the call to the None service in a catch
statement even when configuring that service's operations as
one-way operations, because one-way operations can still throw delivery
exceptions.
The Service and None transaction modes are somewhat esoteric. They are useful in the context of the particular scenarios I've mentioned, but in other scenarios they harbor the danger of jeopardizing the system's consistency. You should typically use the Client/Service or Client transaction mode. Choose between these two based on the ability of the service to be used standalone (that is, based on the consistency consequences of using the service in its own transaction, and the potential for a deadlock). Avoid the Service and None modes.
Although WCF is responsible for every aspect of transaction propagation and the overall management of the two-phase commit protocol across the resource managers, it does not itself know whether a transaction should commit or abort. WCF simply has no way of knowing whether the changes made to the system state are consistent (that is, if they make sense). Every participating service must vote on the outcome of the transaction and voice an opinion about whether the transaction should commit or abort. In addition, WCF does not know when to start the two-phase commit protocol; that is, when the transaction ends and when all the services are done with their work. That too is something the services (actually, just the root service) need to indicate to WCF. WCF offers two programming models for services to vote on the outcome of the transaction: a declarative model and an explicit model. As you will see, voting is strongly related to completing and ending the transaction.
WCF can automatically vote on behalf of a service to commit or abort the
transaction. Automatic voting is controlled via the Boolean TransactionAutoComplete
property of the OperationBehavior
attribute:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute,... { public bool TransactionAutoComplete {get;set;} //More members }
The TransactionAutoComplete
property defaults to
true
, so these two definitions are
equivalent:
[OperationBehavior(TransactionScopeRequired = true,TransactionAutoComplete = true
)]
public void MyMethod( )
{...}
[OperationBehavior (TransactionScopeRequired = true)]
public void MyMethod( )
{...}
When this property is set to true
, if there were
no unhandled exceptions in the operation, WCF will automatically vote to commit the
transaction. If there was an unhandled exception, WCF will vote to abort the
transaction. Note that even though WCF has to catch the exception in order to abort the
transaction, it then rethrows it, allowing it to go up the call chain.
To rely on automatic voting, the service method must have TransactionScopeRequired
set to true
,
because automatic voting only works when it was WCF that set the ambient transaction for
the service.
It is very important when TransactionScopeRequired
is set to true
to avoid catching and handling exceptions and explicitly voiding to abort:
//Avoid
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{
try
{
...
}
catch
{
Transaction.Current.Rollback( )
;
}
}
Even though your service catches the exception, the operation will still result in
an exception since WCF will throw an exception such as TransactionAbortedException
on the client side. WCF does that because your
service could be part of a much larger transaction that spans multiple services,
machines, and sites. All other parties involved in this transaction are working hard,
consuming system resources and locking out other parties, yet it is all in vain because
your service has voted to abort, and nobody knows about it. By returning an exception to
the client WCF ensures that the exception will abort all objects in its path, eventually
reaching the root service or client and terminating the transaction. This will improve
throughput and performance. If you want to catch the exception for some local handling
such as logging, make sure to rethrow it:
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod( )
{
try
{
...
}
catch
{
/* Some local handling here */
throw;
}
}
Explicit voting is required when TransactionAutoComplete
is set to false
.
You can only set TransactionAutoComplete
to false
when TransactionScopeRequired
is set to true
.
When declarative voting is disabled, WCF will vote to abort all transactions by
default, regardless of exceptions or a lack thereof. You must explicitly vote to commit
using the SetTransactionComplete( )
method of the
operation context:
public sealed class OperationContext : ... { public void SetTransactionComplete( ); //More members }
Make sure you do not perform any work, especially transactional work, after the call
to SetTransactionComplete( )
. Calling SetTransactionComplete( )
should be the last line of code in
the operation just before it returns:
[OperationBehavior(TransactionScopeRequired = true,TransactionAutoComplete = false
)] public void MyMethod( ) { /* Do transactional work here, then: */ OperationContext.Current.SetTransactionComplete( )
; }
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.
Not performing any work after SetTransactionComplete(
)
ensures that any exception raised before the call to SetTransactionComplete( )
will cause SetTransactionComplete( )
to be skipped, so WCF will default
to aborting the transaction. As a result, there is no need to catch the exception,
unless you want to do some local handling. As with declarative voting, since the method
aborts, WCF will return a TransactionAbortedException
to the client. In the interest of readability, if you do catch the exception, make sure
to rethrow it:
[OperationBehavior(TransactionScopeRequired = true,TransactionAutoComplete = false
)] public void MyMethod( ) { try { /* Do transactional work here, then: */ OperationContext.Current.SetTransactionComplete( ); } catch { /* Do some error handling then */throw;
} }
Explicit voting is designed for the case when the vote depends on other information obtained throughout the transaction (besides exceptions and errors). However, for the vast majority of applications and services, you should prefer the simplicity of declarative voting.
Setting TransactionAutoComplete
to false
should not be done lightly. In fact, it is only
allowed for per-session services with required session mode, because it has drastic
effects on the service instance's affinity to a transaction. (In order to obtain
information for the vote throughout a transaction, it must be the same transaction and
the same instance.) You will see later why, when, and how you can set TransactionAutoComplete
to false
.
When the transaction ends is determined by who starts it. Consider a client that
either does not have a transaction or just does not propagate its transaction to the
service. If that client calls a service operation configured with TransactionScopeRequired
set to true
, that service operation becomes the root of the transaction. The root
service can call other services and propagate the transaction to them. The transaction
will end once the root operation completes the transaction, which it can do either
declaratively by setting TransactionAutoComplete
to
true
, or explicitly by setting it to false
and calling SetTransactionComplete( )
. This is partly why both TransactionAutoComplete
and SetTransactionComplete( )
are named the way they are; they are used for
more than just voting; they complete and terminate the transaction for a root service.
Note, however, that any of the downstream services called by the root operation can only
use them to vote on the transaction, not to complete it. Only the root both votes on and
completes the transaction.
When a non-service client starts the transaction, the transaction ends when the client disposes of the transaction object. You will see more on that in the section on explicit transaction programming.
In general, the more isolated transactions are, the more consistent their results will
be. The highest degree of isolation is called Serializable.
At this level, the results obtained from a set of concurrent
transactions are identical to the results that would be obtained by running each
transaction serially. To achieve this goal, all the resources a transaction touches must
be locked from any other transaction. If other transactions try to access those resources,
they are blocked and cannot continue executing until the original transaction commits or
aborts.
The isolation level is defined using the IsolationLevel
enumeration, found in the System.Transactions
namespace:
public enum IsolationLevel
{
Serializable
,
RepeatableRead,
ReadCommitted,
ReadUncommitted,
Snapshot, //Special form of ReadCommitted supported by SQL 2005/2008
Chaos, //No isolation whatsoever
Unspecified
}
The difference between the four isolation levels (ReadUncommitted
, ReadCommitted
, RepeatableRead
, and Serializable
) is in the way the different levels use read and write locks. A
lock can be held only while the transaction is accessing the data in the resource manager,
or it can be held until the transaction is committed or aborted: the former is better for
throughput; the latter for consistency. The two kinds of locks and the two kinds of
operations (read/write) give four basic isolation levels. However, not all resource
managers support all levels of isolation, and they may elect to take part in the
transaction at a higher level than the one configured for it. Every isolation level apart
from Serializable
is susceptible to some sort of
inconsistency resulting from more than one transaction accessing the same
information.
Selecting an isolation level other than Serializable
is commonly used for read-intensive systems, and it requires a
solid understanding of transaction processing theory and of the semantics of the
transaction itself, the concurrency issues involved, and the consequences for system
consistency. The reason other isolation levels are available is that a high degree of
isolation comes at the expense of overall system throughput, because the resource managers
involved have to hold on to both read and write locks for as long as a transaction is in
progress, and all other transactions are blocked. However, there are some situations where
you may be willing to trade system consistency for throughput by lowering the isolation
level. Imagine, for example, a banking system where one of the requirements is to retrieve
the total amount of money in all customer accounts combined. Although it would be possible
to execute that transaction with the Serializable
isolation level, if the bank has hundreds of thousands of accounts, it might take quite a
while to complete. The transaction might also time out and abort, because some accounts
could be accessed by other transactions at the same time. However, the number of accounts
may be a blessing in disguise. On average (statistically speaking), if the transaction is
allowed to run at a lower transaction level, it may get the wrong balance for some
accounts, but those incorrect balances will tend to cancel each other out. The actual
resulting error may be acceptable for the bank's needs.
In WCF, the isolation level is a service behavior, so all service operations use the
same configured isolation level. Isolation is configured via the TransactionIsolationLevel
property of the ServiceBehavior
attribute:
[AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute,... { public IsolationLevel TransactionIsolationLevel {get;set;} //More members }
You can only set the TransactionIsolationLevel
property if the service has at least one operation configured with TransactionScopeRequired
set to true
. There is no way to configure the isolation level in the host
configuration file.
The default value of TransactionIsolationLevel
is
IsolationLevel.Unspecified
, so these two statements
are equivalent:
class MyService : IMyContract {...} [ServiceBehavior(TransactionIsolationLevel = IsolationLevel.Unspecified)] class MyService : IMyContract {...}
When a service configured with IsolationLevel.Unspecified
joins the client transaction, the service will
use the client's isolation level. However, if the service specifies an isolation level
other than IsolationLevel.Unspecified
, the client
must match that level, and a mismatch will throw an exception.
When the service is the root of the transaction and the service is configured with
IsolationLevel.Unspecified
, WCF will set the
isolation level to IsolationLevel.Serializable
. If
the root service provides a level other than IsolationLevel.Unspecified
, WCF will use that specified level.
The introduction of isolation locks raises the possibility of a deadlock when one
transaction tries to access a resource manager owned by another. If a transaction takes a
long time to complete, it may be indicative of a transactional deadlock. To address that
possibility, the transaction will automatically abort if it takes longer than a
predetermined timeout (60 seconds, by default) to complete, even if no exceptions took
place. Once it's aborted, any attempt to flow that transaction to a service will result in
an exception. The timeout is a service behavior property, and all operations across all
endpoints of the service use the same timeout. You configure the timeout by setting the
TransactionTimeout
time-span string
property of ServiceBehaviorAttribute
:
[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : Attribute,...
{
public string
TransactionTimeout
{get;set;}
//More members
}
For example, you would use the following to configure a 30-second timeout:
[ServiceBehavior(TransactionTimeout
= "00:00:30")]
class MyService : ...
{...}
You can also configure the transaction timeout in the host config file by creating a
custom behavior
section and referencing it in the
service
section:
<services>
<service name = "MyService" behaviorConfiguration = "ShortTransactionBehavior">
...
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name = "ShortTransactionBehavior"
transactionTimeout = "00:00:30"
/>
</serviceBehaviors>
</behaviors>
The maximum allowed transaction timeout is 10 minutes, and this value will be used even when a larger value is specified. If you want to override the default maximum timeout of 10 minutes and specify, say, 40 minutes, add (or modify) the following in machine.config:
<configuration> <system.transactions> <machineSettings maxTimeout = "00:40:00"/> </system.transactions> </configuration>
Setting any value in machine.config will affect all applications on the machine.
Configuring such a long timeout is useful mostly for debugging, when you want to try to isolate a problem in your business logic by stepping through your code and you do not want the transaction you're debugging to time out while you figure out the problem. Be extremely careful with using a long timeout in all other cases, because it means there are no safeguards against transaction deadlocks.
You may also want to set the timeout to a value less than the default 60 seconds. You typically do this in two cases. The first is during development, when you want to test the way your application handles aborted transactions. By setting the timeout to a small value (such as one millisecond), you can cause your transactions to fail so you can observe your error-handling code.
The second case where it can be useful to set the transaction timeout to less than the default value is when you have reason to believe that a service is involved in more than its fair share of resource contention, resulting in deadlocks. If you are unable to redesign and redeploy the service, you want to abort the transaction as soon as possible and not wait for the default timeout to expire.
When a transaction flows into a service that is configured with a shorter timeout than that of the incoming transaction, the transaction adopts the service's timeout and the service gets to enforce the shorter timeout. This behavior is designed to support resolving deadlocks in problematic services, as just discussed. When a transaction flows into a service that is configured with a longer timeout than the incoming transaction, the service configuration has no effect.
[4] I first presented my designation of transaction propagation modes in the article "WCF Transaction Propagation" (MSDN Magazine, May 2007).
18.223.206.225