The ambient transaction of the service will be null, even though the mandatory transaction flow guarantees the client's transaction propagation. In order 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 OperationBehaviorAttribute:

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:

If the client transaction is propagated to the service, WCF will set the client transaction as the operation's ambient transaction. If not, WCF creates 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 transaction, and you cannot ask WCF to scope it with a transaction. Unless you manually create a new ambient transaction (as shown later on), do not perform transactional work in the service constructor.

Figure demonstrates which transaction a WCF service uses as a product of the binding configuration, the contract operation, and the local operation behavior attribute.

Transaction propagation as product of contract, binding, and operation behavior

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. 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. Since Service 4 does not require a transaction scope, it has no ambient transaction.

Transaction Propagation Modes

Which transaction the service uses is the product of 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. Figure lists the remaining eight permutations.[*]

[*] I first presented my designation of transaction propagation modes in MSDN Magazine, May 2007.

Transaction modes as 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 with only four transaction propagation modes. I call these four modes Client/Service, Client, Service, and None. Figure 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 greatly simplifies thinking about and configuring transaction support.

Client/Service transaction

The Client/Service mode, as its name implies, ensures the service uses the client 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 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 the service minimizes its assumptions about what the client is doing. The service will join the client transaction if the client has a transaction to flow. Joining the client transaction is always good for overall system consistency. Imagine the service has a transaction separate from that of the client. It opens the way for one of these two transactions to commit while the other one aborts, and leave the system in an inconsistent state. When the service joins the client 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 the transaction, and so this mode provides a contingent transaction to the service, by making it the root of a new transaction. Figure shows a service configured for the Client/Service transaction mode.

Note in Figure that the service can assert it always has a transaction. The service cannot assume or assert whether or not 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 deadlocksif the resulting transaction is a service-side transaction, it may deadlock with other transactions trying to access the same resources, because the resources would 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 or when it is joining the client's transaction.

Requiring transaction flow

The Client/Service mode requires the use of a transaction-aware binding with transaction flow enabled, and yet this is not enforced by WCF at the service load time. To tighten this loose screw, you can use my BindingRequirementAttribute:

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 set to TRansactionFlowOption.Allowed, the BindingRequirement attribute will enforce that the endpoint uses a transaction-aware binding with the TRansactionFlow property set to TRue:

The BindingRequirementAttribute class is a service behavior, and 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. For each operation, it accesses its collection of operation behaviors. All operation behaviors implement the IOperationBehavior interface, including the transactionFlowAttribute. If the behavior is TRansactionFlowAttribute, Validate( ) checks if the attribute is configured for transactionFlowOption.Allowed. If so, Validate( ) checks the binding. For each transaction-aware binding, it verifies that the binding has the transactionFlow property set to true, and if not, it will throw an InvalidOperationException. Validate( ) also throws an InvalidOperationException if a nontransactional binding is used for that endpoint.

The technique shown in Figure for implementing BindingRequirementAttribute is a general-purpose technique you can use to enforce any binding requirement. For example, BindingRequirementAttribute has another property called WCFOnly that enforces the use of WCF-to-WCF bindings only and the ReliabilityRequired property that insists on using a reliable binding with reliability enabled:

Client transaction

The Client mode ensures the service only uses the client's transaction. To configure this mode:

Select a transactional binding and enable 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 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 avoid deadlocks and maximize overall system consistency. 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 so there will not be another transaction that competes for access to the same resources and underlying locks. By having a single transaction you maximize consistency, because that transaction will commit or abort as one atomic operation. Figure shows a service configured for the Client transaction mode.

Note in Figure that the method asserts the fact that the ambient transaction is a distributed one, meaning it originated with the client.

Service transaction

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 of the transactionFlow property or explicitly set it to false.

Do not apply the transactionFlow attribute or set it with transaction flow set to transactionFlowOption.NotAllowed.

Set the TRansactionScopeRequired property of the operation behavior to true.

You select the Service transaction mode when the service needs to perform transactional work outside the scope of the client's transaction. For example, when you want to perform some logging or audit operations, or when you want to publish events to subscribers regardless of whether your client transaction commits or aborts. For example, consider a logbook service that performs error logging into a database. When an error occurs on the client side, the client would use the logbook service to log it or some other entries. In case of an error, after logging, the error on the client side aborts the client's transaction. If the service were to use the client transaction, once the client 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. By configuring the service to have its own transaction, logging the error would be committed even when the client transaction aborts. The downside is of course the potential for jeopardizing the system consistency, because the service transaction could abort while the client's commits.

The heuristic you need to make when selecting this mode is that the service 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, as opposed to business transactions that may fail due to 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 transaction and the service's transaction) do not jeopardize consistency if one aborts and the other commits. Logging and auditing services are the classic candidates for this mode.

Note in Figure: the service can assert that it actually has a local transaction.

None transaction

The None transaction mode means 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 of the transactionFlow property or explicitly set it to false.

Do not apply the TRansactionFlow attribute or set it with transaction flow set to transactionFlowOption.NotAllowed.

No need to set the TRansactionScopeRequired property of the operation behavior, and if you do, 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 the operations fail. For example, a service that prints a receipt for a money transfer should not be able to abort the client transaction if 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, such as calling legacy code as in Figure. Obviously, there is danger in the None mode because it can jeopardize the system consistency: if the calling client has a transaction and it calls a service configured as None then the client aborted its transaction, and changes made to the system state by the service will not roll back. Another pitfall of this mode is when a service configured for None calls another service configured for a Client transaction. Such a call will fail because the calling service has no transaction to propagate.

Note that the service in Figure can assert it has no ambient transaction.

The None mode allows you to have a nontransactional service called by a transactional client. As stated previously, configuring for the None mode is mostly for nice-to-have operations. The problem with that is that any exception thrown by the None service will abort the calling client's transaction, something that should be avoided with a 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, calling the service from Figure:

You need to encase the call to the None service in a catch statement even when configuring the operations of the None service as one-way operations, because one-way operations could still throw delivery exceptions.

Choosing a service transaction mode

Out of the four modes, the Service and None modes are somewhat esoteric. They are useful in the context of the particular scenarios mentioned, but other than that they harbor the danger of jeopardizing the system consistency. You should use the Client/Service or Client transaction modes, and choose the mode based on the ability of the service to be used standalone as a function of potential deadlocks and consistency. Avoid the Service and None modes.

Voting and Completion

Although WCF is responsible for every aspect of the transaction propagation and overall management of the two-phase commit protocol across the resource managers, it does not know whether the 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.

Declarative voting

WCF can automatically vote on behalf of the service to commit or abort the transaction. Automatic voting is controlled via the Boolean transactionAutoComplete property of the OperationBehavior attribute:

When 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 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 who 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:

The reason is that your service could be part of a much larger transaction that spans multiple services, machines, and sites. All other parties of this transaction are working hard, consuming system resources; yet it is all in vain because your service voted to abort, and nobody knows about it. By allowing the exception to go up the call chain, it will abort all objects in its path, eventually reaching the root service or client and terminating the transaction. By not handling the exception, you improve throughput and performance. If you want to catch the exception for some local handling such as logging, make sure to rethrow it:

Explicit voting

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 lack thereof. You must explicitly vote using the SetTransactionComplete( ) method of the operation context:

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 returning:

If you try to perform any transactional work (including accessing transaction.Current) after the call to SetTransactionComplete( ), WCF will throw InvalidOperationException and abort the transaction.

By not performing any work after SetTransactionComplete( ), any exception before the call to SetTransactionComplete( ) would skip over it and have WCF 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, if you do catch the exception, make sure to rethrow it so that it will expedite aborting the transaction:

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, and in fact it is only allowed for a per-session service, because it has drastic effects on the affinity of the service instance to a transaction. In order to obtain information for the vote throughout the transaction, it must be the same transaction and the same instance. You will see later on why, when, and how you can set transactionAutoComplete to false.

Terminating a transaction

When the transaction ends is a product of who starts it. Consider a client that either does not have a transaction or just does not propagate its transaction to the service, and 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. The root operation can complete the transaction 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 arethey do more than mere voting; they complete and terminate the transaction for a root service. Note that any of the downstream services called by the root operation can only vote on the transaction, not complete it. Only the root both votes and completes the transaction.

When a nonservice 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.

Transaction Isolation

In general, the more isolated the transactions, the more consistent their results are. The highest degree of isolation is called serializable, meaning the results obtained from a set of concurrent transactions are identical to the results obtained by running each transaction serially. To achieve serialization, all the resources a transaction touches are 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. Isolation level is defined using the IsolationLevel enumeration defined in the System.Transactions namespace:

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 when the transaction accesses 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. In addition, 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. Every isolation level besides serializable is susceptible to some sort of inconsistency resulting from other transactions 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 the semantics of the transaction itself, the concurrency issues involved, and the consequences for system consistency. The reason isolation configuration is 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. One of the requirements is to retrieve the total amount of money in all customer accounts combined. Although it is possible to execute that transaction with the serializable isolation level, if the bank has hundreds of thousands of accounts, it may take quite a while to complete. The transaction may possibly time out and abort, because some accounts are likely being accessed by other transactions at the same time. But 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 on some accounts, but those incorrect balances would tend to cancel each other out. The actual resulting error may be acceptable for the bank's need.

In WCF, the isolation is a service behavior, so that all methods on the service use the same configured isolation. Isolation is configured via the TRansactionIsolationLevel property of the ServiceBehavior attribute:

There is no way to configure isolation level in the host configuration file. You can only set the transactionIsolationLevel property if the service has at least one operation configured with transactionScopeRequired set to TRue.

Isolation and transaction flow

The default value of transactionIsolationLevel is IsolationLevel.Unspecified, so these two statements are equivalent:

When the service joins the client transaction and the service is configured for IsolationLevel.Unspecified, 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 a FaultException on the client's side.

When the service is the root of the transaction and the service is configured for 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.

Transaction Timeout

Due to the use of isolation, the introduction of the isolation locks raises the possibility of a deadlock when one transaction tries to access a resource manager owned by another. If the transaction takes a long time to complete, it may be indicative of a transactional deadlock. To address that, the transaction will automatically abort if executed for more than a predetermined timeout (60 seconds by default). Once aborted, any attempt to flow that transaction to a service will result in an exception. Even if no exceptions take place, the transaction will eventually abort. All that the participating clients and services do is complete the transaction. The transaction time-out is configurable both programmatically and administratively.

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:

The maximum allowed transaction timeout is 10 minutes. The value of 10 minutes is used even when larger values are specified. If you want to override the default maximum timeout of 10 minutes, and specify, say, 30 minutes, add the following to machine.config:

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 long timeouts in all other cases, because it means there are no safeguards against transaction deadlocks.

You typically set the timeout to a value less than the default 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 cause your transaction to fail and can thus observe your error-handling code.

The second case in which you set the transaction timeout to be less than the default timeout is when you have reason to believe that a service is involved in more than its fair share of resource contention, resulting in deadlocks. In that case, you want to abort the transaction as soon as possible and not wait for the default timeout to expire.

Transaction flow and timeout

When a transaction flows into a service that is configured with a shorter timeout than the incoming transaction, the transaction adopts the service's timeout, and the service gets to enforce the shorter timeout. This 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.