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