Transactions

In an enterprise system, maintaining the integrity of data across various applications and machines is critical. Regardless of the scope of the application, at least some aspects of transaction processing have to be implemented to guarantee the integrity of the data. However, developing code to handle data integrity can be very challenging. COM+ provides a service called automatic transaction processing that simplifies this development effort. In this section, we look at how to develop .NET serviced components that use COM+ transaction services.

A Simple Banking System

We need an example to explore the transaction support under COM+. In our example, a customer, Jay, has an account at two banks—Fidelity and Schwab. Jay wants to transfer some money from his account at Fidelity to his account at Schwab.

The Databases

Each bank stores customer balances in a database table Accounts. The table contains two columns, Pin to store the unique account number of the customer and Balance to store the current balance.

We will use Microsoft Desktop Engine (MSDE) as our database server. It is a Microsoft SQL Server-compatible database engine that ships with .NET Framework SDK (as well as some other products).

MSDE comes with a command-line program, osql.exe. We use this tool to create two databases, FidelityDB and SchwabDB. The FidelityDB database stores Jay's balance under the account number FID-3456 and the SchwabDB stores the balance under the account number SCH-4567. The initial balance in both accounts is $100,000.

To simplify the creation of the databases, I have created an SQL batch file that contains the following SQL commands:

-- File Transactions/DBCreation/CreateAccounts.cmd
create database FidelityDB
go
use FidelityDB
create table Accounts ([Pin] varchar (15) NOT NULL,
     [Balance] int NOT NULL)
create unique index Pin on Accounts([Pin])
insert into Accounts Values ('FID-3456', '100000')
go
create database SchwabDB
go
use SchwabDB
create table Accounts ([Pin] varchar (15) NOT NULL,
     [Balance] int NOT NULL)
create unique index Pin on Accounts([Pin])
insert into Accounts Values ('SCH-4567', '100000')
go
quit

You can submit this batch file as input to OSQL as follows:

osql -S .NetSDK -E -i CreateAccounts.sql

Here, .NetSDK identifies the instance of MSDE on the local machine and switch -E informs OSQL to use Windows authentication to connect to the MSDE, eliminating the need for a separate user name and password.

The Coding Logic

Our banking system example consists of three serviced components:

  1. Component FidelityBank represents the banking activity at Fidelity.

  2. Component SchwabBank represents the banking activity at Schwab.

  3. Component TransferFund is a utility component that provides a utility method, FromFidelityToSchwab, to transfer funds from a Fidelity account to a Schwab account.

Here is the relevant code excerpt for each of these components:

// Project Transactions/Banks

public class FidelityBank : ServicedComponent, IDisposable {
     private MyAccountsDB m_db;

     public void WithdrawMoney(String pin, int amount) {
       int balance = m_db.GetBalance(pin);
       m_db.UpdateBalance(pin, balance - amount);
     }
     ...
}

public class SchwabBank : ServicedComponent, IDisposable {
     private MyAccountsDB m_db;

     public void AddMoney(String pin, int amount) {
       int balance = m_db.GetBalance(pin);
       m_db.UpdateBalance(pin, balance+amount);
     }
     ...
}

public class TransferFunds : ServicedComponent {

     public void FromFidelityToSchwab(String fidelityPin,
          int amount, String schwabPin) {
       using(FidelityBank fB = new FidelityBank()) {
          using (SchwabBank sB = new SchwabBank()) {
            fB.WithdrawMoney(fidelityPin, amount);
            sB.AddMoney(schwabPin, amount);
          }
       }
     }
}

Class MyAccountsDB that is being used in this code isolates the use of the database and provides useful methods such as GetBalance (to obtain the balance for an account) and UpdateBalance (to update the balance for an account). It uses ADO.NET to access the database. Here is the code for the class in its entirety. Here I am giving you an opportunity to learn the basics of ADO.NET in two minutes or less:

public class MyAccountsDB : IDisposable {
     private SqlConnection m_Conn;
     public MyAccountsDB(String dbName) {
       String s = String.Format(
"server=(local)\NetSDK;Trusted_Connection=yes;database={0}",
          dbName);
       m_Conn = new SqlConnection(s);
       m_Conn.Open();
     }

     public int GetBalance(String pin) {
       String s = String.Format(
          "SELECT Balance FROM Accounts WHERE [Pin] = '{0}'",
            pin);
       using (SqlCommand cmd = new SqlCommand(s, m_Conn)) {
          using (SqlDataReader reader = cmd.ExecuteReader()) {

            if (!reader.Read()) {
               s = String.Format("Unknown pin: '{0}'", pin);
               throw new Exception(s);
            }
            int balance = (int) reader["Balance"];
            return balance;
          }
       }
     }

     public void UpdateBalance(String pin, int balance) {
       String s = String.Format(
       "UPDATE Accounts SET Balance = {0} WHERE [Pin] = '{1}'",
            balance, pin);
       using (SqlCommand cmd = new SqlCommand(s, m_Conn)) {
          int numRecords = cmd.ExecuteNonQuery();
          if (0 == numRecords) {
            s = String.Format("Unknown pin: '{0}'", pin);
            throw new Exception(s);
          }
       }
     }

     //
     // Standard Dispose pattern
     //

     ~MyAccountsDB() {
       Dispose(false);
     }

     public void Close() {
       Dispose();
     }

     public void Dispose() {
       Dispose(true);
       GC.SuppressFinalize(this);
     }

     // Always dispose unmanaged resources
     // Disposing==true => dispose managed resource as well
     protected virtual void Dispose(bool disposing) {
       if (disposing) {
          // dispose managed resources
          if (null != m_Conn) {
            // Must always close the connection
            m_Conn.Close();
            m_Conn = null;
          }
       }
     }
}

It is time for us to configure the serviced components to participate in a transaction. However, let's first examine the requirements for a transaction.

Theory of Transaction

For our banking example, a transfer transaction consists of two operations:

  1. Reduce the balance for the account in the FidelityDB database.

  2. Add to the balance for the account in the SchwabDB database.

A transaction must be such that it entirely succeeds or entirely fails. This implies that all of the operations involved in the transaction must be updated successfully or nothing should be updated at all. This all-or-nothing proposition of a transaction is called atomicity.

A transaction must be consistent. Any individual operation within a transaction may leave the data in such a state that it violates the system's integrity. In our case, after the completion of the first operation, some money has been taken out of the system. After the completion of the second operation, either the system should rollback to the original state (restore the money that was taken out), or, on success, go to a new state that still maintains the overall integrity of the system.

The system should isolate any uncommitted changes. A second transaction that happens concurrently should only be able to see the data in the state before the first transaction begins or in the state after the first transaction completes, but not in some half-done mode between the two states.

Finally, a transaction must be durable; that is, when a transaction is committed, the data sources involved must guarantee that the updates will persist, even if the computer crashes (or the power goes off) immediately after the commit. This requires specialized transaction logging that would allow the data source's restart procedure to complete any unfinished operations.

Atomicity, consistency, isolation, and durability: A transaction should support these properties. This is the ACID test for transactions.

Configuring the Serviced Components

Using the COM+ transaction support under .NET is a two-step process:

1.
Each serviced component that intends to participate in a transaction needs to indicate its interest.

2.
Each serviced component method that participates in a transaction should vote for either committing or aborting the transaction. There are three ways to vote in a transaction, and we will look at each of them next.

Enabling Transaction Support

A serviced component indicates its interest in participating in a transaction by means of an attribute, TransactionAttribute. A constructor for this attribute takes a parameter of enumeration type TransactionOption. Here is what each of the options means:

  • TransactionOption.Required: This value implies that a component must have a transaction to do its work. If the component's object is activated within the context of an existing transaction, the transaction is propagated to the new object. If the activator's context has no transactional information, then COM+ creates a brand new context containing transactional information and attaches it to the object.

  • TransactionOption.RequiresNew: Sometimes an object might wish to initiate a new transaction, regardless of the transactional status of its activator. When a RequiresNew value is specified, COM+ initiates a new transaction that is distinct from the activator's transaction. The outcome of the new transaction has no affect on the outcome of the activator's transaction.

  • TransactionOption.Supported: A component with this value indicates that it does not care for the presence or absence of a transaction. If the activator is participating in a transaction, the object propagates the transaction to any new object that it activates. The object itself may or may not participate in the transaction. This value is generally used when the component doesn't really need a transaction of its own but wants to be able to work with other components.

  • TransactionOption.NotSupported: The component has no interest in participating in a transaction, regardless of the transactional status of its activator. This guarantees that the component's object neither votes in its activator's transaction nor begins a transaction of its own; nor does it propagate the caller's transaction to any object that it activates. This value should be chosen if you wish to break the continuity of an existing transaction.

  • TransactionOption.Disabled: If a component will never access a resource, setting the transaction attribute to disabled eliminates any transaction-related overhead for the component. This attribute simulates the transaction behavior of a nonconfigured component.

Each of the three serviced components in our example uses the Required transaction option, as illustrated in the following code excerpt:

[Transaction(TransactionOption.Required)]
public class FidelityBank : ServicedComponent, IDisposable {
     ...
}

COM+ automatically begins a transaction when it encounters either of the following conditions:

  1. When a nontransactional client activates an object with a component that has a transaction option set to either Required or RequiresNew.

  2. When a transactional client calls an object with a component that has a transaction option set to RequiresNew.

The object responsible for beginning a new transaction is referred to as the root object of that transaction. As we will see shortly, this root object has a special role in completing the transaction.

An object that subsequently gets activated within the boundary of this transaction, and is marked as either Required or Supported, shares the transaction.

If an object is participating in a transaction, it can obtain its transaction ID from its context, as highlighted in the following code fragment:

[Transaction(TransactionOption.Required)]
public class FidelityBank : ServicedComponent, IDisposable {
     ...
     public void WithdrawMoney(String pin, int amount) {
       Console.WriteLine("Transaction ID: {0}",
          ContextUtil.TransactionId);
       ...
     }
}

A transaction completes when the root object of the transaction is deactivated. At this point, COM+ checks if all the objects have individually given their consent to commit the transaction. If all the participants have committed, COM+ goes ahead and commits the transaction, in which case the databases are updated with the new values. If any participant disapproved of the transaction, the transaction is aborted and the databases are rolled back to their original state.

A transaction is completed only after the root object of the transaction is deactivated. Forcing the clients to release the root object and recreate it for each transaction not only requires some programming effort on the part of the client, but is also inefficient. Marking the root object transactional component as JIT-activated and setting the deactivate-on-return bit within an appropriate method implementation deactivates the root object. Not only does this enforce transaction completion, but it also leaves the setup (the proxy, the COM communication channel, etc.) intact. In fact, JIT activation is so crucial for transactional correctness that, if a component is marked to participate in a transaction, COM+ ensures that the component is also automatically enabled for JIT activation.

Voting Using MyTransactionVote

A serviced component can use a static property MyTransactionVote, to vote on the transaction's outcome. This property is of enumeration type TransactionVote with two possible options—Commit and Abort.

The following code excerpt demonstrates the use of MyTransactionVote:

// Class SchwabBank
public void AddMoney(String pin, int amount) {
     try {
       int balance = m_db.GetBalance(pin);
       m_db.UpdateBalance(pin, balance+amount);
       ContextUtil.MyTransactionVote =
								    TransactionVote.Commit;
     }catch(Exception e) {
       ContextUtil.MyTransactionVote =
								    TransactionVote.Abort;
       throw e; // propagate the error
     }finally {
       ContextUtil.DeactivateOnReturn = true;
     }
}

Note that, irrespective of the outcome of the transaction, the deactivate-on-return bit must always be set to true.

It is also possible to avoid the exception-handling code with a little programming trick. Start with setting the deactivate-on-return bit to true and the transaction vote to abort. All that is needed when returning from the method is to set the transaction vote to commit. Using this logic, the preceding implementation of AddMethod can be rewritten as follows:

public void AddMoneyEx(String pin, int amount) {
     ContextUtil.DeactivateOnReturn = true;
     ContextUtil.MyTransactionVote =
       TransactionVote.Abort;
     int balance = m_db.GetBalance(pin);
     m_db.UpdateBalance(pin, balance+amount);
     ContextUtil.MyTransactionVote =
       TransactionVote.Commit;
}

Voting Using SetComplete

It is possible to combine the two operations, voting for the transaction and setting the deactivate-on-return bit to true, into one. This is done by means of two static methods that are available on the class ContextUtilSetComplete (to commit the transaction) and SetAbort (to abort the transaction). The following code excerpt illustrates the use of these methods:

// Class FidelityBank
public void WithdrawMoney(String pin, int amount) {
     try {
       int balance = m_db.GetBalance(pin);
       if (balance < amount) {
          String s = String.Format(
          "Client '{0}' does not have enough balance",pin);
          throw new Exception(s);
       }
       m_db.UpdateBalance(pin, balance - amount);
       ContextUtil.SetComplete();
     }catch(Exception e) {
       ContextUtil.SetAbort();
       throw e; // propagate the error
     }
}

Note that there is no need to explicitly set the deactivate-on-return bit. The implementation of SetComplete as well as SetAbort set this bit to true internally.

It is also possible to rearrange this code to avoid the exception-handling logic. The trick is to start with calling SetAbort first, as shown here:

public void WithdrawMoneyEx(String pin, int amount) {
     ContextUtil.SetAbort(); // to start with
     int balance = m_db.GetBalance(pin);
     if (balance < amount) {
       String s = String.Format(
          "Client '{0}' does not have enough balance",pin);
       throw new Exception(s);
     }
     m_db.UpdateBalance(pin, balance - amount);
     ContextUtil.SetComplete();
}

Declarative Voting

Finally, perhaps the easiest way to participate in a transaction is by marking a method with an attribute, AutoCompleteAttribute, and enabling it to true, as illustrated in the following code excerpt:

// Class TransferFunds
[AutoComplete(true)]
public void FromFidelityToSchwab(String fidelityPin,
       int amount, String schwabPin) {
     using(FidelityBank fB = new FidelityBank()) {
       using (SchwabBank sB = new SchwabBank()) {
          fB.WithdrawMoney(fidelityPin, amount);
          sB.AddMoney(schwabPin, amount);
       }
     }
}

When a method is marked with AutoCompleteAttribute, COM+ automatically calls SetComplete if the method call returns normally. If the method call throws an exception, then COM+ automatically aborts the transaction, as if SetAbort was called.

Finally, here is the code excerpt for the client application that wishes to transfer the money:

// Project Transactions/MyClient

public static void TransferFunds() {
     try {
       using (TransferFunds tf = new TransferFunds()) {
       // tf.FromFidelityToSchwab("FID-3456", 100, "SCH-4567");
         tf.FromFidelityToSchwab("FID-3456", 100, "SCH-4568");
       }
       Console.WriteLine("Transfer success");
     }catch(Exception e) {
       Console.WriteLine("Transfer failed: {0}",e.Message);
     }
}

Using the account number SCH-4568 instead of SCH-4567 causes the transaction to fail. You can verify this by checking the tables in the databases.

At this point, you must have a fairly good understanding of how a serviced component can participate in a transaction. There is one thing that might have puzzled you. It makes sense to have FidelityBank and SchwabBank as two different serviced components. However, why do we need a third serviced component, TransferFunds? It seems the logic of transferring funds could have been implemented in the client application itself.

By using the TransferFunds object as the root of the transaction, and instantiating the two bank components in the context of the same transaction, we tie the operations from two banks together as a single transaction. Had the client application implemented the logic of instantiating the bank objects and transferring the funds between them, it would have resulted in two separate and independent transactions. The outcome of one transaction would not affect the outcome of the other. You can witness this behavior by temporarily removing the TransactionAttribute on TransferFunds and dumping the transaction ID from each of the methods involved in the transaction.

Essentially, the reason the client application had to rely on a serviced component was that the client itself could not initiate the transaction.

Extending Transactions to Clients

It is possible to set a nonserviced component to participate in a transaction. The COM+ library defines a class, TransactionContext, that the client can use for this purpose. When this class is instantiated, it automatically starts a transaction. Other objects can be instantiated in the same transaction context by means of a method, CreateInstance, on the TransactionContext object. The client itself must vote on the transaction by calling either Commit or Abort on the TransactionContext object. The following client-side code excerpt demonstrates the use of this class:

// Project Transactions/MyClient

public static void ParticipateInTransaction() {
     // Get the transaction object
     TransactionContext tc = new TransactionContext();
     // Create fidelity bank object and invoke
     // WithdrawMoney method on it using Reflection
     Object oF = tc.CreateInstance("Fidelity.Bank");
     Type tF = oF.GetType();
     MethodInfo mF = tF.GetMethod("WithdrawMoney");
     mF.Invoke(oF, new Object[]{"FID-3456", 100});

     // Create schwab bank object and invoke
     // AddMoney method on it using Reflection
     Object oS = tc.CreateInstance("Schwab.Bank");
     Type tS = oS.GetType();
     MethodInfo mS = tS.GetMethod("AddMoney");
     mS.Invoke(oS, new Object[]{"SCH-4568", 100});

     // Everything is OK. Commit
     tc.Commit();
     Console.WriteLine("Transfer success ...");
}

Note that the default behavior for the TransactionContext object is to abort the transaction. The client has to explicitly call Commit to indicate its positive intentions. Obviously, all the participating transactional objects also have to approve of the transaction for it to commit.

Also note that this specific code relies on Reflection to invoke the methods. This is because the proxy object that is returned by CreateInstance does not support the original type. Casting the object to its original type, as shown in the following code, causes the runtime to throw an exception:

Object oF = tc.CreateInstance("Fidelity.Bank");
FidelityBank fB = (FidelityBank) oF;

The only other way to avoid using Reflection is to encapsulate the functionality of the serviced component in an interface. The object returned by CreateInstance can then be cast to an appropriate interface and methods can be invoked from the interface.

Extending Transactions to Web Services

The .NET Framework makes it possible for a Web service method to participate in a transaction. Recall from Chapter 6 that a Web service method needs to be marked with an attribute, WebMethodAttribute. An overloaded constructor of this attribute takes a parameter of type TransactionOption. The following code excerpt for a Web service implements our logic of transferring funds from Fidelity to Schwab:

// File Transactions/TransferFunds.asmx

[WebService(Namespace="http://localhost/TransactionDemo/")]
public class TransferFunds : WebService {

     [WebMethod(TransactionOption = TransactionOption.Required)]
     public void FromFidelityToSchwab(String fidelityPin,
          int amount, String schwabPin) {
       using(FidelityBank fB = new FidelityBank()) {
          using (SchwabBank sB = new SchwabBank()) {
            fB.WithdrawMoney(fidelityPin, amount);
               sB.AddMoney(schwabPin, amount);
          }
       }
     }
}

Note that Web service methods need not explicitly vote on a transaction. If an exception occurs within a Web service method, the transaction is automatically aborted (as if SetAbort was called). If the method returns successfully without throwing an exception, then the transaction is automatically committed (as if SetComplete was called). Of course, the Web service can still use ContextUtil to explicitly vote on the transaction.

Also note that for this particular example to work, you also need to turn client impersonation on via Web.Config and configure IIS virtual directly for Windows authentication. Otherwise, the call to open the database connection might fail.

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

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