Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#
Article

Creating COM+ Objects using EnterpriseServices in .NET

Rate me:
Please Sign up or sign in to vote.
4.74/5 (37 votes)
23 Mar 200314 min read 267.4K   3.2K   103   40
An article on using .NET to create COM+ distributed components acting with a transaction across multiple databases.

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.

VB
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
   
   '
   ' Do something to the database
   '
   
   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.

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

Image 2

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

C#
using System;
using System.EnterpriseServices;
using MyBusiness.Personnel;
using MyBusiness.Orders;
[assembly: ApplicationName("MyBusiness.Administration")]
[assembly: ApplicationActivation(ActivationOption.Library)]

namespace MyBusiness.Administration
{
    /// <summary>
     /// Epmployee Administration Object
     /// </summary>
   
     [ Transaction(TransactionOption.RequiresNew) ]
     [ ObjectPooling(true, 5, 10) ]
     public class EmployeeMaintenance : ServicedComponent
     {
         public EmployeeMaintenance()
         {
             //
             // TODO: Add constructor logic here
            //
         }

        [ AutoComplete(true) ]
          public void AddEmployee(string Name, string Address, int JobType, <BR>                                  bool bMakePayrollFail, bool bMakeOrdersFail)
        {
             // Create out tier 3 of 4 components that act as the data access layer.
            PayrollMaintenance payroll_maintenance = new PayrollMaintenance();
             OrdersMaintenance orders_maintenance = new OrdersMaintenance();
            
            // Some business Logic...Names must always be stored in upcase!
             Name = Name.ToUpper();
            
            // Let the tier 3 of 4 access the seperate databases and store 
            // our complex example business information.
            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.

C#
void Activate();
bool CanBePooled();
void Construct(string s);
void Deactivate();

Moving on, our EmployeeMaintenance class has two attributes associated with it.

C#
[ 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.

C#
[ AutoComplete(true) ]
public void AddEmployee(string Name, string Address, int JobType, <BR>                        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:

C#
public void SampleFunction()
{
    try
    { 
        // Do something to a database
        // ...
        // Everything okay so far Commit the transaction
        
        ContextUtil.SetComplete();
    }
    catch(Exception)
    {
        // Something went wrong Abort and Rollback the Transaction.
        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.

C#
using System;
using System.Data;
using System.Data.OleDb;
using System.EnterpriseServices;


[assembly: ApplicationName("MyBusiness.Personnel")]
[assembly: ApplicationActivation(ActivationOption.Library)]

namespace MyBusiness.Personnel
{    
    /// <summary>
    /// Payroll Specific Mainenance Object
    /// </summary>
    [ Transaction(TransactionOption.Required) ]
    [ ObjectPooling(true, 5, 10) ]
    public class PayrollMaintenance : ServicedComponent
    {
        public PayrollMaintenance ()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        public void AddEmployee(string Name, string Address, int JobType, <BR>                                bool MakeFail)
        {
            string sConnection = "Provider=SQLOLEDB;Data Source=localhost;" +<BR>                                 "Initial Catalog=MyPersonnelDB;" +<BR>                                 "Trusted_Connection=Yes";
            OleDbConnection cnn= new OleDbConnection(sConnection);<BR>
            // Open the Database Connection
            cnn.Open();
        
            // .....    
            // Open the DataAdapter, DataSets and open a new Row
            // See the code in the example
            // .....    
    
            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");

            // Close the Database Connection
            cnn.Close();

            if(MakeFail)
            {
                // Oh no!!! Its all gone horibly wrong.
                throw new Exception("User requested Exception in " + <BR>                                    "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.

C#
[ 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.

Image 3
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

C#
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]

Becomes

C#
[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 NameTypeLengthOther
nEmployeeID
int 4Primary Key & Index
sName nvarchar50
sAddressnvarchar200
nJobType int4
sTransactionActivityID nvarchar50
sTransactioContextID nvarchar50

MyOrdersDB

tblOrderUser

Field NameTypeLengthOther
nEmployeeID
int 4Primary Key & Index
sName nvarchar50
nJobType int4
sTransactionActivityID nvarchar50
sTransactioContextID nvarchar50

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:

Image 4

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.

Image 5

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

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


Written By
Web Developer
United Kingdom United Kingdom
Not surprisingly, Giles is a software developer. Currently he works in finance, but previously worked in a company doing Physics and Engineering studies/research. Outside of programming and work he enjoys Physics & Astronomy and everything else scientific. Also he has a heathly addiction to books. Amazon, being his dealer of choice, known for praying on other vulerable book addicts all over the world. Oh yes, last but not least playing the guitar and drums when he gets time.

Comments and Discussions

 
GeneralRegarding to createobject from remote COM+ application Pin
roja110-Jul-08 5:58
roja110-Jul-08 5:58 
GeneralDeploying Presentation Layer and COM + in two servers Pin
Sylvester george26-Jun-07 4:35
Sylvester george26-Jun-07 4:35 
QuestionA problem in rollback, using Com+ for remote objects Pin
Ilia Mav9-Feb-07 21:00
Ilia Mav9-Feb-07 21:00 
QuestionHow can I build a out-of-process .NET Enterprise Services component Pin
kosmas18-Jan-07 18:38
kosmas18-Jan-07 18:38 
Questionmissing AutoComplete(true) ? Pin
petercli26-Sep-06 4:50
petercli26-Sep-06 4:50 
QuestionWhat abt ASP.NET article Pin
adityap2-Aug-06 20:35
adityap2-Aug-06 20:35 
GeneralThank you. Pin
Member 101701129-Aug-05 6:34
Member 101701129-Aug-05 6:34 
GeneralOleDbException: No error information available: E_FAIL Pin
RenderEngSol28-Mar-05 2:41
RenderEngSol28-Mar-05 2:41 
GeneralRe: OleDbException: No error information available: E_FAIL Pin
Harish Devanathan8-Aug-08 7:57
Harish Devanathan8-Aug-08 7:57 
GeneralTimeout Expired Pin
Tahir Raza9-Mar-05 20:23
Tahir Raza9-Mar-05 20:23 
GeneralRe: Timeout Expired Pin
Giles11-Mar-05 23:47
Giles11-Mar-05 23:47 
Are you calling Begin Transaction more than once?

You only need to call it once, before you start speaking to each of the databases.

Also, because I've not seen your code, you can try changing from Transaction.RequiresNew on your object to RequiresExisting, off the top of my head.

Anyway, its been about a year since I've used this stuff so I'm a bit rusty.
GeneralGood Article and Simple one Pin
Anil Jain4-Jul-04 23:16
Anil Jain4-Jul-04 23:16 
GeneralTransaction on Deletion of files Pin
Nitin Atreya26-Apr-04 18:28
Nitin Atreya26-Apr-04 18:28 
GeneralRe: Transaction on Deletion of files Pin
Giles28-Apr-04 11:07
Giles28-Apr-04 11:07 
GeneralRe: Transaction on Deletion of files Pin
Mikedev12-Apr-05 9:33
Mikedev12-Apr-05 9:33 
GeneralDistributed Transaction Pin
Karthikeyan Ganesan3-Feb-04 1:33
Karthikeyan Ganesan3-Feb-04 1:33 
GeneralA problem I encountered Pin
Ægidius Ahenobarbus9-Jan-04 12:04
Ægidius Ahenobarbus9-Jan-04 12:04 
GeneralGreat article Pin
Jeff Varszegi16-Oct-03 9:15
professionalJeff Varszegi16-Oct-03 9:15 
GeneralWindows Server 2003, DTC, COM+, and Message Queuing Pin
johncarneiro@verizon.net15-Oct-03 7:27
johncarneiro@verizon.net15-Oct-03 7:27 
General[Message Deleted] Pin
Member 101701129-Aug-05 6:13
Member 101701129-Aug-05 6:13 
GeneralNice! Pin
Mazdak28-Aug-03 22:14
Mazdak28-Aug-03 22:14 
GeneralRe: Nice! Pin
Giles28-Aug-03 22:29
Giles28-Aug-03 22:29 
GeneralGreat Article!! Pin
Nayr19-Aug-03 20:16
Nayr19-Aug-03 20:16 
GeneralRe: Great Article!! Pin
Giles20-Aug-03 8:06
Giles20-Aug-03 8:06 
GeneralRe: Great Article!! Pin
Anonymous20-Aug-03 16:19
Anonymous20-Aug-03 16:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.