Service Transaction Behavior

The Transactions sample demonstrates the use of a client-coordinated transaction and the settings of ServiceBehaviorAttribute and OperationBehaviorAttribute to control service transaction behavior. This sample is based on the Getting Started that implements a calculator service, but is extended to maintain a server log of the performed operations in a database table and a stateful running total for the calculator operations. Persisted writes to the server log table are dependent upon the outcome of a client coordinated transaction - if the client transaction does not complete, the Web service transaction ensures that the updates to the database are not committed.

Note

The setup procedure and build instructions for this sample are located at the end of this topic.

The contract for the service defines that all of the operations require a transaction to be flowed with requests:

[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples",
                    SessionMode = SessionMode.Required)]
public interface ICalculator
{
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Mandatory)]
    double Add(double n);
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Mandatory)]
    double Subtract(double n);
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Mandatory)]
    double Multiply(double n);
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Mandatory)]
    double Divide(double n);
}

To enable the incoming transaction flow, the service is configured with the system-provided wsHttpBinding with the transactionFlow attribute set to true. This binding uses the interoperable WSAtomicTransactionOctober2004 protocol:

<bindings>
  <wsHttpBinding>
    <binding name="transactionalBinding" transactionFlow="true" />
  </wsHttpBinding>
</bindings>

After initiating both a connection to the service and a transaction, the client accesses several service operations within the scope of that transaction and then completes the transaction and closes the connection:

// Create a client
CalculatorClient client = new CalculatorClient();

// Create a transaction scope with the default isolation level of Serializable
using (TransactionScope tx = new TransactionScope())
{
    Console.WriteLine("Starting transaction");

    // Call the Add service operation.
    double value = 100.00D;
    Console.WriteLine("  Adding {0}, running total={1}",
                                        value, client.Add(value));

    // Call the Subtract service operation.
    value = 45.00D;
    Console.WriteLine("  Subtracting {0}, running total={1}",
                                        value, client.Subtract(value));

    // Call the Multiply service operation.
    value = 9.00D;
    Console.WriteLine("  Multiplying by {0}, running total={1}",
                                        value, client.Multiply(value));

    // Call the Divide service operation.
    value = 15.00D;
    Console.WriteLine("  Dividing by {0}, running total={1}",
                                        value, client.Divide(value));

    Console.WriteLine("  Completing transaction");
    tx.Complete();
}

Console.WriteLine("Transaction committed");

// Closing the client gracefully closes the connection and cleans up resources
client.Close();

On the service, there are three attributes that affect the service transaction behavior, and they do so in the following ways:

  • On the ServiceBehaviorAttribute:

    • The TransactionTimeout property specifies the time period within which a transaction must complete. In this sample it is set to 30 seconds.

    • The TransactionIsolationLevel property specifies the isolation level that the service supports. This is required to match the client's isolation level.

    • The ReleaseServiceInstanceOnTransactionComplete property specifies whether the service instance is recycled when a transaction completes. By setting it to false, the service maintains the same service instance across the operation requests. This is required to maintain the running total. If set to true, a new instance is generated after each completed action.

    • The TransactionAutoCompleteOnSessionClose property specifies whether outstanding transactions are completed when the session closes. By setting it to false, the individual operations are required to either set the OperationBehaviorAttribute.TransactionAutoComplete property to true or to explicitly require a call to the OperationContext.SetTransactionComplete() method to complete transactions. This sample demonstrates both approaches.

  • On the ServiceContractAttribute:

    • The SessionMode property specifies whether the service correlates the appropriate requests into a logical session. Because this service includes operations where the OperationBehaviorAttribute TransactionAutoComplete property is set to false (Multiply and Divide), SessionMode.Required must be specified. For example, the Multiply operation does not complete its transaction and instead relies upon a later call to Divide to complete using the SetTransactionComplete method; the service must be able to determine that these operations are occurring within the same session.
  • On the OperationBehaviorAttribute:

    • The TransactionScopeRequired property specifies whether the operation's actions should be executed within a transaction scope. This is set to true for all operations in this sample and, because the client flows its transaction to all operations, the actions occur within the scope of that client transaction.

    • The TransactionAutoComplete property specifies whether the transaction in which the method executes is automatically completed if no unhandled exceptions occur. As previously described, this is set to true for the Add and Subtract operations but false for the Multiply and Divide operations. The Add and Subtract operations complete their actions automatically, the Divide completes its actions through an explicit call to the SetTransactionComplete method, and the Multiply does not complete its actions but instead relies upon and requires a later call, such as to Divide, to complete the actions.

The attributed service implementation is as follows:

[ServiceBehavior(
    TransactionIsolationLevel = System.Transactions.IsolationLevel.Serializable,
    TransactionTimeout = "00:00:30",
    ReleaseServiceInstanceOnTransactionComplete = false,
    TransactionAutoCompleteOnSessionClose = false)]
public class CalculatorService : ICalculator
{
    double runningTotal;

    public CalculatorService()
    {
        Console.WriteLine("Creating new service instance...");
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public double Add(double n)
    {
        RecordToLog(String.Format(CultureInfo.CurrentCulture, "Adding {0} to {1}", n, runningTotal));
        runningTotal = runningTotal + n;
        return runningTotal;
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public double Subtract(double n)
    {
        RecordToLog(String.Format(CultureInfo.CurrentCulture, "Subtracting {0} from {1}", n, runningTotal));
        runningTotal = runningTotal - n;
        return runningTotal;
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    public double Multiply(double n)
    {
        RecordToLog(String.Format(CultureInfo.CurrentCulture, "Multiplying {0} by {1}", runningTotal, n));
        runningTotal = runningTotal * n;
        return runningTotal;
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    public double Divide(double n)
    {
        RecordToLog(String.Format(CultureInfo.CurrentCulture, "Dividing {0} by {1}", runningTotal, n));
        runningTotal = runningTotal / n;
        OperationContext.Current.SetTransactionComplete();
        return runningTotal;
    }

    // Logging method omitted for brevity
}

When you run the sample, the operation requests and responses are displayed in the client console window. Press ENTER in the client window to shut down the client.

Starting transaction
Performing calculations...
  Adding 100, running total=100
  Subtracting 45, running total=55
  Multiplying by 9, running total=495
  Dividing by 15, running total=33
  Completing transaction
Transaction committed
Press <ENTER> to terminate client.

The logging of the service operation requests are displayed in the service's console window. Press ENTER in the client window to shut down the client.

Press <ENTER> to terminate service.
Creating new service instance...
  Writing row 1 to database: Adding 100 to 0
  Writing row 2 to database: Subtracting 45 from 100
  Writing row 3 to database: Multiplying 55 by 9
  Writing row 4 to database: Dividing 495 by 15

Note that in addition to keeping the running total of the calculations, the service reports the creation of instances (one instance for this sample) and logs the operation requests to a database. Because all of the requests flow the client's transaction, any failure to complete that transaction results in all of the database operations being rolled back. This can be demonstrated in a number of ways:

  • Comment out the client's call to tx.Complete() and rerun - this results in the client exiting the transaction scope without completing its transaction.

  • Comment out the call to the Divide service operation - this results prevent the action initiated by the Multiply operation from completing and thus the client's transaction ultimately also fails to complete.

  • Throw an unhandled exception anywhere in the client's transaction scope - this similarly prevents the client from completing its transaction.

The result of any of these is that none of the operations performed within that scope are committed and the count of rows persisted to the database do not increment.

Note

As part of the build process the database file is copied to the bin folder. You must look at that copy of the database file to observe the rows that are persisted to the log rather than the file that is included in the Visual Studio project.

To set up, build, and run the sample

  1. Ensure that you have installed SQL Server 2005 Express Edition or SQL Server 2005. In the service's App.config file, the database connectionString may be set or the database interactions may be disabled by setting the appSettings usingSql value to false.

  2. To build the C# or Visual Basic .NET edition of the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  3. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.

If you run the sample across machines, you must configure the Microsoft Distributed Transaction Coordinator (MSDTC) to enable network transaction flow and use the WsatConfig.exe tool to enable Windows Communication Foundation (WCF) transactions network support.

To configure the Microsoft Distributed Transaction Coordinator (MSDTC) to support running the sample across machines

  1. On the service machine, configure MSDTC to allow incoming network transactions.

    1. From the Start menu, navigate to Control Panel, then Administrative Tools, and then Component Services.

    2. Right-click My Computer and select Properties.

    3. On the MSDTC tab, click Security Configuration.

    4. Check Network DTC Access and Allow Inbound.

    5. Click Yes to restart the MS DTC service and then click OK.

    6. Click OK to close the dialog box.

  2. On the service machine and the client machine, configure the Windows Firewall to include the Microsoft Distributed Transaction Coordinator (MSDTC) to the list of excepted applications:

    1. Run the Windows Firewall application from Control Panel.

    2. From the Exceptions tab, click Add Program.

    3. Browse to the folder C:\WINDOWS\System32.

    4. Select Msdtc.exe and click Open.

    5. Click OK to close the Add Program dialog box, and click OK again to close the Windows Firewall applet.

  3. On the client machine, configure MSDTC to allow outgoing network transactions:

    1. From the Start menu, navigate to Control Panel, then Administrative Tools, and then Component Services.

    2. Right-click My Computer and select Properties.

    3. On the MSDTC tab, click Security Configuration.

    4. Check Network DTC Access and Allow Outbound.

    5. Click Yes to restart the MS DTC service and then click OK.

    6. Click OK to close the dialog box.