Transactional Service Programming

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.

Setting the Ambient Transaction

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.

Warning

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.

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

Figure 7-6. Transaction propagation as the 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 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.

Transaction Propagation Modes

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.

Client/Service transaction mode

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:

  1. Select a transactional binding and enable transaction flow by setting TransactionFlow to true.

  2. Set the transaction flow option in the operation contract to TransactionFlowOption.Allowed.

  3. 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.

Requiring transaction flow

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.

Tip

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;}
}

Client transaction mode

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

  1. Select a transactional binding and enable transaction flow by setting TransactionFlow to true.

  2. Set the transaction flow option in the operation contract to TransactionFlowOption.Mandatory.

  3. 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.

Service transaction mode

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:

  1. 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.

  2. Do not apply the TransactionFlow attribute, or configure it with TransactionFlowOption.NotAllowed.

  3. 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.

None transaction mode

If the None transaction mode is configured, the service never has a transaction. To configure this mode:

  1. 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.

  2. Do not apply the TransactionFlow attribute, or configure it with TransactionFlowOption.NotAllowed.

  3. 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
{}

Tip

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.

Choosing a service transaction mode

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.

Voting and Completion

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.

Declarative voting

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

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.

Warning

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.

Terminating a transaction

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.

Transaction Isolation

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.

Isolation and transaction flow

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.

Transaction Timeout

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 stringTransactionTimeout
   {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>

Tip

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.

Transaction flow and timeout

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.141.27