Introduction
Okay, this is my first article on CodeProject, but I hope it does not show. I have taken my time, having had what I thought were many good ideas for articles along the way, but being me being me, I never got around to them because I spend so much time in the lounge. Enough of the excuses, I just hope that you enjoy and find the following useful.
Background
I had been wanting to get to grips with .NET in a more demanding environment, namely where workloads need to be spread between machines, but also where data at all costs cannot become corrupted. From my C++ experience I know that this would involve building ATL COM+ components, and implementing all kids of interfaces. There seems to be an an easier way with the .NET Enterprise services that I felt are not touched on enough, and that the MSDN documentation is lacking. So my aim was to provide a quick tour of putting a sample distributed database transaction together and walling though some of the details. I imagine there are things I forgotten to explain, so let me know and I will try and expand on it.
A Brief History of Distributed Transactions on Windows
I am assuming that you are familiar with database transactions following the typical Begin, Commit and Rollback Transaction cycle. e.g.
Sub UpdateDB()
Dim cnn As New ADODB.Connection
Dim rs As New ADODB.Recordset
cnn.Open "Northwind", "Admin", "Password"
cnn.BeginTrans
rs.Open "tblOrders", cnn, adOpenDynamic, adLockOptimistic
rs.Close
cnn.CommitTrans
cnn.Close
Exit Sub
err_handler:
cnn.RollbackTrans
cnn.Close
End Sub
Figure 1 - Show ADO Example - Please excuse the Visual Basic, but it�s a quick example
Around the time of Windows NT4 the Distributed Transaction Coordinator (DTC) was introduced as part of SQL Server 6.5. Later the Microsoft Transaction Server (MTS) was introduced which encompassed the DTC, that provided the ability for middle-tier server-side COM object components to exist in a load balanced distributed system. These could take part in a transaction and vote on if they had completed successfully what they had been asked to do as part of the overall application in the transaction. An MTS component that a developer created could be remotely administered, with such features as set NT user security permissions at the interface level of each MTS COM object.
Later in Windows 2000, MTS evolved into COM+. This had the added advantage that the security was more fine-grained, so that an admin could grant/revoke access to particular users at the method level. Previously it had not been unheard of for users to have to implement extra security because the permission levels were not detailed enough. From now on I will assume that you are targeting a Windows 2000 system or later and refer to COM+.
With the advent of .NET, if it was to be used in an enterprise environment, then it needed to be stable, fault tolerant and secure. So why not just put those bits on top of what was already a very good framework � COM+. The EnterpriseService
namespace (which you need to add a reference to by default) contains all you will need to perform distributed transactions. The only point to note here was that the .NET COM inter-op need to be dealt with as cleanly as possible, which gladly it was. Programming for COM+ in C++ and VB required implementing quite a few interfaces in your classes. In .NET you just derive your class from the ServicedComponent object which takes quite a bit of the drudgery away.
Why a distributed transaction?
In today�s large and demanding business applications were data has to be extracted from a database, complex calculations made against it with business rules applied, finalised with various record being updated, can be far too much work for one machine. Further to that, even with smaller apps hosting them on a user�s workstation from a disaster recovery point of view would worry most IT managers. COM+ provides as scalable, fault tolerant platform, in that a middle tier COM objects can be created, and a server or cluster of servers work it out between them where this lives on the network.
N.B. Fault tolerance is something that has been greatly enhanced in Windows 2003 Server, which has 8 node clustering and fail over, and can cope with 3 of the machines failing, where the transaction continues unhindered.
Also there is another problem COM+ solves. What happens when you need to make changes to 2 or more different databases located on different servers as part of a transaction? e.g.
You have a Personnel Database and Sales/Orders Database. They were implemented separately because it was a requirement to keep an employee�s personal data such as their salary and bonus info from the payroll completely separate from the every day business database that most employees use in order processing. That or from a legacy point of view it was made that way, and they will not let you change it (sounds familiar).
Now you as a developer have been asked to implement a reliable feature as part of an application for adding and removing employees to both database as part of the required business logic, and you need to do it as a transaction � both databases are updated or no databases at all is something goes wrong.
But I hear you scream, if I am using my ADO connection objects, which can only connect to one database at a time, and has its own Begin, Commit and Rollback Transaction methods. Do I need to run two connections, and handle what looks like a messy implementation myself?
The answer you are glad to hear is no. Under COM+, you don�t even need to call Begin, Commit/Rollback on the connections as they are already aware that they are running in a transaction context with the DTC has already told them about. So there is also less code to write from one perspective.
All of this relies on the fact that at the bottom of the tier is a database platform that can be instructed by a DTC. SQL Server, Oracle and DB2 all play nicely. Sybase however does not as it has no clue.
Implementing a Sample COM+ object in .NET
I�am going to show you how to implement a very simple set of objects that can work under and as part of a transaction, and where one object will be the root business object that starts a new transaction context.
New Transaction Required
� Employee Maintenance (Root object from the client app perspective)
Existing Transaction Required or Creates a New Transaction (if it is called directly by the client)
� Payroll Maintenance
� Orders Maintenance
I�am not going to go into any details on how to manage security on a COM+ app's (maybe a later article), and for this example will assume that it is all running under the same user account, so should cause no problems. Despite this it is easy to configure. In Windows 2000 or XP, go to the Control Panel, Administrative Tools, and Component Services (See Figure 6). This is the COM+ Manager. Bring up your local computer and browse to the local COM+ Application Packages, which contain the COM objects. In these you can view the COM Interfaces they implement and the methods on those components.
The Basic Layout of the Sample Application
The following is my attempt at trying to diagram the layout of the system.
Figure 2- Basic Implementation Layout
We are going to build a simple thick client WinForm application that will allow the user to add a user on both databases as part of a single transaction. It would be just as easy to write a web based admin page using ASP.NET that consumes this middle tier COM+ objects, but perhaps that is for another article.
For a new employee the business requires that we store :
� Name
� Address
� Job type
Yes not much, but it serves as an example. Both the Personnel department and Orders department database want to know about the employees name and job type, but only the Personnel department needs to know the employees home address to post their payslip to.
At this point you should create a blank solution in Visual Studio .NET and give it the catchy name of MyBusinessSolution. To that add three C# class libraries:
� Administration
� Personnel
� Orders
I laid it out with three libraries because at a later date I may want to add other components for doing more than setting up employees. After all, the company is not going to make much money otherwise. This way I will have an COM+ application silo for each department.
We can use the default class objects, but it will be worthwhile to place each of the default namespace's into our company name MyBusiness Ltd. You will need to add the EnterpriseServices
namespace for each project. To do this, go to the menu Project -> Add Reference or Solution Explorer and right click the projects references tab and select Add Reference. This brings up the add references dialog shown below. Select the System.EnterpriseServices
component.
Figure 3 - Add/Remove Project References
Also you will have to add the MyBusiness.Personnel
and MyBusiness.Orders
projects and namespaces to the MyBusiness.Administration project as the Admin object will call the departmental maintenance objects.
On to some code - The Administration Object
As if by magic, the main administration business object presents itself. Now this is all very contrived, but stay with me. I'm going to point out some of the features that turn a class into a class that can be used in COM+.
using System;
using System.EnterpriseServices;
using MyBusiness.Personnel;
using MyBusiness.Orders;
[assembly: ApplicationName("MyBusiness.Administration")]
[assembly: ApplicationActivation(ActivationOption.Library)]
namespace MyBusiness.Administration
{
[ Transaction(TransactionOption.RequiresNew) ]
[ ObjectPooling(true, 5, 10) ]
public class EmployeeMaintenance : ServicedComponent
{
public EmployeeMaintenance()
{
}
[ AutoComplete(true) ]
public void AddEmployee(string Name, string Address, int JobType,
bool bMakePayrollFail, bool bMakeOrdersFail)
{
PayrollMaintenance payroll_maintenance = new PayrollMaintenance();
OrdersMaintenance orders_maintenance = new OrdersMaintenance();
Name = Name.ToUpper();
payroll_maintenance.AddEmployee(Name, Address, JobType, bMakePayrollFail);
orders_maintenance.SetupUser(Name, JobType, bMakeOrdersFail);
}
}
}
Our EmployeeMaintenance
class is derived from the ServicedComponent
class which is central to the object living happily under and making use of the COM+ runtimes services. ServicedComponent
is derived from ContextBoundObject
which is in turn is derived from MarshalByRefObject
.
ServicedComponent
has the following overrides, which you may find useful at a later date.
void Activate();
bool CanBePooled();
void Construct(string s);
void Deactivate();
Moving on, our EmployeeMaintenance
class has two attributes associated with it.
[ Transaction(TransactionOption.RequiresNew) ]
[ ObjectPooling(true, 5, 10) ]
This object requires that it exist in its own new transaction regardless of whether it was created under another transaction context. In the other components that this class users, they are marked as Requires. They need a transaction, but are happy to use the one they are created in, otherwise they will create a new one. That is how two on more databases can share the same transaction.
For the client, this class has only one method which would be of interest called AddEmployee
.
[ AutoComplete(true) ]
public void AddEmployee(string Name, string Address, int JobType,
bool bMakePayrollFail, bool bMakeOrdersFail)
The method is marked with the AutoComplete
attribute that implements a useful feature, to say that if no exception is thrown then mark its part of the transaction as being okay. This helps cut down on the amount of code required. If the implementation sets AutoComplete
to false, or omits it all together, then we would need to manage the transaction manually. To manually control the transaction you will need to use the ContextUtil
class and its static members which I recommend having a look at on MSDN. A short exert follows showing how to use the ContextUtil
class manually:
public void SampleFunction()
{
try
{
ContextUtil.SetComplete();
}
catch(Exception)
{
ContextUtil.SetAbort();
}
}
Manually controlling the transaction as an alternative to [AutoComplete(true)]
The SetComplete
and SetAbort
methods of the ContextUtil
class act on two of its properties. These are:
ContextUtil.MyTransactionVote
- Used to mark the transaction as being okay so far.
ContextUtil.DeactivateOnReturn
� Says no more to be done, either Commit or Rollback with respect to MyTransactionVote
.
If you are planning to build an object that calls multiple functions in the object before committing the transaction, then it would be quite important to use the more fine-grained approach rather than make it as AutoComplete.
Back to our class, I want to point out that there are two parameters on the end of the function, that allow the client application to signal one of the components to throw an Exception. I wanted this to easily demonstrate that even after one of the components used had opened a database modified it and closed the connection successfully as far as it was concerned, the data had still not been committed until the COM+ runtime had said so. What it was waiting for was the root object that owned the transaction to tell it to commit the transaction as part of the overall context.
The Personnel Maintenance Class
I will not show the whole listing, just parts.
using System;
using System.Data;
using System.Data.OleDb;
using System.EnterpriseServices;
[assembly: ApplicationName("MyBusiness.Personnel")]
[assembly: ApplicationActivation(ActivationOption.Library)]
namespace MyBusiness.Personnel
{
[ Transaction(TransactionOption.Required) ]
[ ObjectPooling(true, 5, 10) ]
public class PayrollMaintenance : ServicedComponent
{
public PayrollMaintenance ()
{
}
public void AddEmployee(string Name, string Address, int JobType,
bool MakeFail)
{
string sConnection = "Provider=SQLOLEDB;Data Source=localhost;" +
"Initial Catalog=MyPersonnelDB;" +
"Trusted_Connection=Yes";
OleDbConnection cnn= new OleDbConnection(sConnection);
cnn.Open();
dr["sName"] = Name; ;
dr["sAddress"] = Address;
dr["nJobType"] = JobType;
dr["sTransactionActivityID"] = ContextUtil.ActivityId;
dr["sTransactionContextID"] = ContextUtil.ContextId;
ds.Tables["tblEmployees"].Rows.Add(dr);
da.Update(ds, "tblEmployees");
cnn.Close();
if(MakeFail)
{
throw new Exception("User requested Exception in " +
"PayrollMaintenance.AddEmployee");
}
}
}
}
Now notice that the TransactionOption
is set to Required
. I referred to this earlier saying that the component called by the object that started the transaction, should use the transaction. This allows it to do so, rather than being RequiresNew
which would start another transaction, and spoil things.
[ Transaction(TransactionOption.Required) ]
In the database I also want to store some properties of the ContextUtil
class that represents the current transaction. These are the ActivityID
and ContextID
. As you will see, as I update both databases in the separate objects, they will both see the same Transaction ContextID
.
COM+ Components in .NET Are Strong Named Assemblies
A component derived from ServicedComponent
needs to be strong named if it is to run under COM+. This means that it needs to be digitally signed with a public private key. Well how do I create a key I hear you ask? Quite easy this one - fire up the VS.NET command prompt and have a look at the strong name utility sn.exe as seen below. To build your own strong name pass �k as the first parameter and the name you want for your output signature file.
Figure 4 - Running the Strong Name Utility to create a new key
Once you have the file, for each of the Serviced Component libraries you need to reference it in each of the assembly file belonging to the library project. The attribute needed is automatically populated at the bottom of the AssemblyInfo.cs file, with the strong name key file missing.
So
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]
Becomes
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("..\\..\\..\\MyBusiness.sn")]
[assembly: AssemblyKeyName("")]
The Databases
If you download the sample solution from above, contained with are the scripts to create the database on SQL sever under the folder Database Creation Scripts. There are two databases required for the example, each with one table if you and to do it by hand:
MyPersonnelDB
tblEmployees
Field Name |
Type |
Length |
Other |
nEmployeeID
|
int |
4 |
Primary Key & Index |
sName |
nvarchar |
50 |
|
sAddress |
nvarchar |
200 |
|
nJobType |
int |
4 |
|
sTransactionActivityID |
nvarchar |
50 |
|
sTransactioContextID |
nvarchar |
50 |
|
MyOrdersDB
tblOrderUser
Field Name |
Type |
Length |
Other |
nEmployeeID
|
int |
4 |
Primary Key & Index |
sName |
nvarchar |
50 |
|
nJobType |
int |
4 |
|
sTransactionActivityID |
nvarchar |
50 |
|
sTransactioContextID |
nvarchar |
50 |
|
You will notice that I added two extra fields. These are populated by the two data access layer objects with what they think are the TransactionActivity
and TransactionContexts
which are properties of the ContextUtil
class. All being well, both databases should see the same transaction context.
Running the Application
If you run the app, you will be presented with the following screen:
Figure 5 - Simple Client to test the COM+Application
Remember to enter some sample text, and add the employee. If your machine is getting old like mine then it will grind for a few seconds. This is because the components are initialising for the first time. As these are pooled objects the second time and so on is much faster.
You should then view the contents of the database to see that a new entry has been added to both. If you rerun the above, but with one of the forced exceptions turned on, the who transaction will rollback, even though the database connections have been opened and then closed from the code.
Finally, if you go bring up the Component Manager from the Control Panel -> Administrative Tools, you will see that the 3 component libraries have been created, and from the Transaction Statistics you should be able to see the transactions whiz by, or those that aborted.
Figure 6 - The Component Services Console for administering your COM+ Applications
At the bottom of the image in Figure 6 you should see under Distributed Transaction Coordinator the Transaction Statistics. From this screen you can see the status and result of transactions running on the system Notice that after you have run the client app, and for each time you add and employee, the Transaction Aggregate will go up - your running transactions.
Points of Interest for Further Development
Something that work well that I have not implemented in the code is the use of Message Queues for logging the details of a failed trasaction for an Administrator to diagnose at a later date. The important point to note here would be to not use a Transactional Queue as it would run in the Transaction Context and be rolled back making it a futile exercise. Did I not mention Message Queues can be transactional? Hmm another time.
History
24 Mar 2003 - First Edition