Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET Distributed Transactions on Enterprise Services: a demo

0.00/5 (No votes)
6 May 2004 1  
This demo shows you how to develop .NET components capable of participating in distributed transactions coordinated by .NET Enterprise Services

Sample Image - KdotNET.gif

Introduction

Once upon a time, in the middle of 90s, there was a smart colleague of mine who loved to learn (and to teach) by examples. He was able to create clear, simple and practical examples on every computer-development related topic. These examples (that he used to call "technology demos") were famous amongst all colleagues, and sometimes considered as milestones in the evolution of the company's general know-how. In fact, they were always developed on the latest Microsoft technology, often in beta version.

I remember well, his sample titled (don't ask me why) "K", in which he demonstrated the use of COM+ distributed transaction support, implementing some components in Visual C++ 6.0 and Visual Basic 6.0, and making them work with two SQL Server databases, inside a DTC-orchestrated transaction.

When I faced the ".NET Enterprise Services" topic at the time of .NET Beta 2, I felt the absence of a clear and simple example like the transaction demo "K". But - remembering what my guru did some years before - I decided to write a .NET version of that demo. So, "KdotNET" was born, and now it's here to show you how simple it is to implement .NET classes that can take advantage of the transactional services exposed by COM+, through the .NET Enterprise Services facilities.

Background

The goal of KdotNET is to show how to develop transactional classes enabled to work with .NET Enterprise Services. At a first look, this seems to be the same goal of the well known sample about transaction management included in the .NET Framework SDK (check the folder ...\SDK\v1.1\Samples\Technologies\ComponentServices\Transactions on your .NET Framework installation and you'll find it). In that sample, they show how the programmer can take advantage of the COM+ transaction support when developing a single class that interacts with a single data source (but you can achieve a similar behavior just using Commit() and Rollback() methods of a SqlTransaction object, don't you?). In KdotNET, instead, I actually demonstrate something more interesting: how Enterprise Services manage distributed transactions across multiple data sources, and how the transaction outcome is propagated by COM+ across multiple transacted classes.

To complete our demonstration, we'll consider a practical example in which a distributed transaction has to take place: a simple money transfer between two bank accounts, hosted by different banks, and so also on different databases (this is a very classical example).

We will use, of course, two SQL Server 2000 databases. The first one, named Bank1, will be on the SQL Server "A", and it will contain an Accounts table; this table holds Bank1's accounts. The second database will be Bank2 (on the server "B") and it will contain another Accounts table:

Bank1.Accounts table
ID Description Amount
1 Karl 100
2 Albert 200
3 Ricky 300
 
Bank2.Accounts table
ID Description Amount
1 Donald 1000
2 Mickey 2000
3 Minnie 3000

You can create the two databases on SQL Server 2000 using the CreatingDB.sql script. It creates each database with the default options (for the path and the file size); then it creates (on each database) the Accounts table, with a primary key on the ID field and a UNIQUE constraint on the Description field; then it populate each Accounts table with some sample data.

Also an INSERT / UPDATE trigger is created on the Accounts table; its goal is to check the remaining amount of a single account, so that it is not possible to have negative amounts in the table: any INSERT or UPDATE operation that makes negative the remaining amount of an account will be aborted, rolling back the implicit transaction that wraps the INSERT or UPDATE operation:

  CREATE TRIGGER Accounts_InsUpd 
  ON Accounts 
  FOR INSERT, UPDATE 
  AS
  DECLARE @remaining INT
  -- For simplicity, here the trigger manages one-row operations only

  IF (SELECT COUNT(*) FROM inserted) = 1
    BEGIN
    SELECT @remaining = Amount FROM inserted
    IF @remaining < 0
      BEGIN
      RAISERROR('Insufficient money.', 10, 1)
      ROLLBACK TRANSACTION
      END
    END

In the following paragraphs, we'll suppose to have a money transfer between an account of Bank1 and another account of Bank2; being these accounts on different servers, a distributed transaction will be initiated. We'll execute the credit operation to the target account (for example, on server "B") before the charge operation on the source account (for example, on server "A"), to show how the negative remaining amount on the source account is triggered and how it fires a local ROLLBACK on server "A". We'll demonstrate how this ROLLBACK is propagated to the entire distributed transaction, causing the actual undo of the credit operation already done (or - better - just "prepared" and still waiting for a final COMMIT) on server "B", thanks to the transactional support exposed by Enterprise Services.

The transacted components

For simplicity, the transacted components used in KdotNET are organized in very small, elementary classes, each focused on a basic operation.

The money transfer between bank accounts is managed by the kmvbcls class (assembly: kmvb.dll, namespace: kmvb). This VB.NET class exposes the method:

Public Function transfer(ByVal ConnString1 As String, _
       ByVal ConnString2 As String, _
       ByVal Account1 As String, ByVal Account2 As String, _
       ByVal AmountToTransfer As Integer) As Boolean

Given the connection strings to the two databases, the source and target account names, and the amount of money to transfer, this method actually does the transfer, calling a credit() method and subsequently a charge() method. The charge() method is exposed by the k1vbcls class (assembly: k1vb.dll, namespace: k1vb). The credit() method is exposed by the k2vbcls class (assembly: k2vb.dll, namespace: k2vb).

Here is the code for the kmvbcls class:

<TransactionAttribute(TransactionOption.Required)> _
Public Class kmvbcls
    Inherits ServicedComponent

  ' Transfer some money between two accounts (from Account1 to Account2),

  ' invoking methods from k1vbcls and k2vbcls classes

  ' Return: True if Okay, False otherwise

  Public Function transfer(ByVal ConnString1 As String, _
                    ByVal ConnString2 As String, _
                    ByVal Account1 As String, ByVal Account2 As String, _
                    ByVal AmountToTransfer As Integer) As Boolean
    Dim RetValue As Boolean
    Dim objCredit As New k2vb.k2vbcls
    Dim objCharge As New k1vb.k1vbcls

    Try
      objCredit.credit(ConnString2, Account2, AmountToTransfer)
      objCharge.charge(ConnString1, Account1, AmountToTransfer)
      RetValue = True
      ContextUtil.SetComplete()

    Catch exc As Exception
      RetValue = False
      ContextUtil.SetAbort()
      Throw New Exception("Error in kmvb:" & ControlChars.CrLf & exc.Message)

    Finally
      objCredit.Dispose()
      objCharge.Dispose()

    End Try
    Return RetValue

  End Function

End Class

The implementation of the three classes kmvbcls, k1vbcls and k2vbcls includes some features required to make them usable in the Enterprise Services context:

  • first of all, they all are derived from the System.EnterpriseServices.ServicedComponent class;
  • then, the assembly which hosts each class has been strongly-named (through the use of the <Assembly: AssemblyKeyFileAttribute(...)> attribute referring a key file generated with the SN.EXE utility);
  • finally, each class has been labeled with a <TransactionAttribute(...)> attribute, indicating that COM+ has to support and manage a transaction for that class.

Here is the code for the k1vbcls and k2vbcls classes:

<TransactionAttribute(TransactionOption.Required)> _
Public Class k1vbcls
  Inherits ServicedComponent

  ' Charges on the specified database and account the specified amount

  Public Sub charge(ByVal ConnString As String, _
    ByVal Account As String, ByVal AmountToCharge As Integer)

    Dim strSQL As String
    strSQL = "UPDATE Accounts SET Amount = Amount - " _
             & AmountToCharge.ToString() & _
             " WHERE Description = '" & Account & "'"
    Dim HowManyRows As Integer      ' Number of modified rows

    Dim cnn As New SqlConnection(ConnString)
    Dim cmd As New SqlCommand(strSQL, cnn)

    Try
      cnn.Open()
      HowManyRows = cmd.ExecuteNonQuery()
      If HowManyRows = 1 Then
        ContextUtil.SetComplete()
        Exit Sub
      Else
        ' UPDATE failed

        ContextUtil.SetAbort()
        Throw New Exception("Invalid account or insufficient money.")
      End If

    Catch exc As Exception
      ContextUtil.SetAbort()
      Throw New Exception(exc.Message)

    Finally
      cmd.Dispose()
      cnn.Close()
    End Try

  End Sub

End Class


<TransactionAttribute(TransactionOption.Required)> _
Public Class k2vbcls
  Inherits ServicedComponent

  ' Credits to the specified database and account the specified amount

  Public Sub credit(ByVal ConnString As String, _
    ByVal Account As String, ByVal AmountToCredit As Integer)

    Dim strSQL As String
    strSQL = "UPDATE Accounts SET Amount = Amount + " _
             & AmountToCredit.ToString() & _
             " WHERE Description = '" & Account & "'"
    Dim HowManyRows As Integer      ' Number of modified rows

    Dim cnn As New SqlConnection(ConnString)
    Dim cmd As New SqlCommand(strSQL, cnn)

    Try
      cnn.Open()
      HowManyRows = cmd.ExecuteNonQuery()
      If HowManyRows = 1 Then
        ContextUtil.SetComplete()
        Exit Sub
      Else
        ' UPDATE failed

        ContextUtil.SetAbort()
        Throw New Exception("Invalid account.")
      End If

    Catch exc As Exception
      ContextUtil.SetAbort()
      Throw New Exception(exc.Message)

    Finally
      cmd.Dispose()
      cnn.Close()
    End Try

  End Sub

End Class

To inform COM+ about the outcome of a single part of the transaction, these components make use of SetComplete() and SetAbort() methods from the System.EnterpriseServices.ContextUtil class. An alternative to this manual management of the partial transaction outcome is provided by the .NET Framework through the use of the System.EnterpriseServices.AutoCompleteAttribute attribute (also shown in the SDK sample mentioned above). This attribute instructs COM+ that a given method:

  • will vote positively for the transaction commit (actually, with an implicit SetComplete()) if it completes normally, or
  • will vote for a transaction rollback (actually, with an implicit SetAbort()) if during its execution some exceptions are thrown.

KdotNET shows both alternative ways to achieve the transaction vote from a ServicedComponent class:

  • the SetComplete() / SetAbort() methodology is shown in the three classes kmvbcls, k1vbcls and k2vbcls described earlier;
  • the AutoCompleteAttribute approach is shown in the three classes kmcscls, k1cscls and k2cscls, that are the C# equivalent of kmvbcls, k1vbcls and k2vbcls, provided here for the C# lovers.

How to run the demo

The KdotNET components we just developed are ready to be used, without any registration in COM+. This is due to the fact that the assemblies containing our classes are strongly-named, and to the power of .NET Framework Enterprise Services that provide an automatic registration for classes derived from the ServicedComponent class.

So, all we need is a client that instantiates our classes and invokes their methods; in particular, our final goal will be to use the kmvbcls (or the kmcscls) class, invoking its transfer() method to see if the distributed transaction really works, showing us the all-or-nothing behavior we are expecting. To accomplish this kind of testing, I provided CompTest (that stands for "Component Tester"), a little Windows Forms client that my guru would call a "pulsantiera" (Italian term we used to indicate a form full of un-useful buttons):

As you can see (by its self-describing UI), this simple client allows you to test each single method of our components (in their VB.NET or C# version): by clicking on charge or credit buttons, you can test respectively the elementary operation of charging or crediting the specified amount on the specified bank account; obviously, only by clicking a transfer button you actually start a money transfer between two bank accounts, so initiating a distributed transaction. Keep in mind that setting the "Money to transfer" textbox to a negative value, you can invert the transfer direction (from Bank2 to Bank1 instead of from Bank1 to Bank2).

During this testing activity, of course, you'll need to verify the actual state of the data on the Accounts tables of Bank1 and Bank2. To do so, you can use BankMonitor, a little utility I developed to make simpler the bank account monitoring task. This is another Windows Forms application that queries, at specified intervals, the Accounts tables and shows their content.

BankMonitor reads the Accounts tables without observing database locks (that is: using a "read uncommitted" transaction isolation level). This behavior allows you to see "dirty reads" and to investigate about transient, uncommitted states during the execution of the distributed transaction. So, you can observe that in case of insufficient money on the source bank account, the distributed transaction effectively rolls back. To make the fact evident: modify the transfer() method adding a wait time of 5 seconds between the credit and the charge operation (as shown below), and then try a money transfer of an amount greater than the money availability of the donor.

  ...
  objCredit.credit(ConnString2, Account2, AmountToTransfer)
  System.Threading.Thread.Sleep(5000)
  objCharge.charge(ConnString1, Account1, AmountToTransfer)
  ...

If you look at the BankMonitor window during this money transfer (with a refresh rate less than 5 seconds, let's say 3 seconds), you will see for a moment the target bank account amount increased (of course, this is an uncommitted situation); then it will be decreased as soon as the charge operation fails, causing the k1vb (or k1cs) component to vote for a negative transaction outcome (calling an explicit or implicit SetAbort()) and so causing the ROLLBACK of the entire distributed transaction.

Calling the serviced components from an ASP.NET web page

An alternative way to test our transacted components is inside an ASP.NET Web Form. You can prepare an ASP.NET project, referencing (as private assemblies) kmvb.dll, k1vb.dll and k2vb.dll (or their C# counterparts) and invoking the transfer() method as CompTest did. Running that web project and monitoring its activity with BankMonitor, you will see (surprise!?) that everything works as in the case of the Windows Application client.

But you can also provide a less trivial web sample, if you take advantage from the ability to make transactional the ASPX page itself, adding the Transaction attribute to the @Page directive:

<%@ Page Transaction="Required" Language="vb" Codebehind="WebForm1.aspx.vb" ... %>

With this attribute set, the page itself propagates its transactional context to the called serviced components, so there is no need to use the kmvbcls class to coordinate the distributed transaction. This approach is shown in the WebCompTest (Web Component Tester) project, a simple ASP.NET Web Application containing this Web Form:

The code for the Click event of the "Transfer" button simply instantiates the k1vbcls and k2vbcls classes, and invokes their methods:

  Private Sub cmdTransfer_Click(...) Handles cmdTransfer.Click
    Dim objCredit As New k2vb.k2vbcls
    Dim objCharge As New k1vb.k1vbcls

    Try
      objCredit.credit(txtConn2.Text, _
        txtAccount2.Text, Convert.ToInt32(txtAmount.Text))
      objCharge.charge(txtConn1.Text, _
        txtAccount1.Text, Convert.ToInt32(txtAmount.Text))
      lblResult.Text = "Success (" & System.DateTime.Now().ToString() & ")"

    Catch exc As Exception
      lblResult.Text = "Failure (" & System.DateTime.Now().ToString() & ")" & _
          "<BR>" & exc.Message

    Finally
      objCredit.Dispose()
      objCharge.Dispose()

    End Try
  End Sub

Of course, if you remove the Transaction attribute from the @Page directive (and you avoid the use of the kmvbcls class), the ASPX page will stop to share its transactional context with the called components: so, the objCredit and objCharge objects will run each in its own transaction, being unaware of the outcome of the operations executed by other objects. No distributed transactions will be initiated, and the system will allow Karl to give away money in any case (also with an insufficient remaining amount on his bank account): the charge() method will fail without causing an undo on the previously executed credit() operation. In this situation, the result is that Karl makes money from nothing. Oh, lucky Karl!

Acknowledgments

The idea of KdotNET moves his steps from a sample titled "K - transaction demo", developed some years ago on the Windows DNA architecture by Carlo Randone. To him my thanks for that work and - more important - for the patience he spent in opening my mind to new concepts.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here