Using COM Services with .NET Assemblies

 
Chapter 18 - COM+ Services
bySimon Robinsonet al.
Wrox Press 2002
  

Using COM+ Services with .NET Assemblies

Now that we understand what the various COM+ Services are, let's look at how the services can be used with .NET. The .NET Framework still relies on COM+ to provide run time services to .NET components , and provides this integration with a collection of classes in the System.EnterpriseServices namespace. An Enterprise Service is a COM+ service provided through the System.EnterpriseServices namespace. A serviced component is a .NET class designed to use Enterprise Services, and a serviced assembly is a .NET assembly that contains at least one serviced component.

To create a class as a serviced component, the class must inherit from the ServicedComponent class of the System.EnterpriseServices namespace, or from other classes that derive from this class, and must also define a public default constructor.

   public class ComPlusExample : ServicedComponent     {     public ComPlusExample()     {     }   
Important 

Note that to use the System.EnterpriseServices namespace, you need to manually add a reference to the System.EnterpriseServices.dll , either with Project Add Reference from Visual Studio .NET, or by adding /r:System.EnterpriseServices.dll if compiling from the command line.

By prefixing class definitions with attributes defined in the System.EnterpriseServices namespace, you're able to specify how COM+ Services treat those classes. The C# compiler knows how to translate the attributes into the necessary code "hooks" that COM+ Services expect from components.

The attributes defined in the System.EnterpriseServices namespace include:

  • Transaction

  • ObjectPooling

  • JustInTimeActivation

  • EventClass

  • ApplicationActivation

In addition to these attributes, the System.EnterpriseServices namespace defines various classes and enumerations as well, several of which we'll examine in detail shortly.

The following sections explain how we can use .NET Enterprise Service classes in our applications.

Configuring Assemblies

First, we need to add a set of assembly attributes to the assembly in AssemblyInfo.cs of any Enterprise Services application that we wish to use:

   [assembly:ApplicationActivation(ActivationOption.Server)]     [assembly:ApplicationID("448934a3-324f-34d3-2343-129ab3c43b2c")]     [assembly:ApplicationName("EnterpriseServiceApplication")]     [assembly:Description("Description of your assembly here.")]   

Let's examine each of these attributes in turn .

Remember earlier when we mentioned that there are two kinds of COM+ applications, server applications and library applications? The first attribute in the code sample, the ApplicationActivation attribute, allows you to specify which of these two kinds of applications a particular assembly is. The acceptable values for this attribute are defined in the ActivationOption enumeration. By specifying the application's type programmatically with this attribute, there is no need to open up the Component Services manager and do so manually. This enumeration has two values, ActivationOption.Library and ActivationOption.Server .

The second attribute, ApplicationID , is the GUID of the application. The ApplicationName attribute allows you to specify the name of the COM+ Services application that will be created to host the .NET assembly when the assembly is imported into COM+ Services. In our example, we've used the value EnterpriseServiceName . The Description attribute allows you to associate a description with the assembly.

Deploying an Assembly for COM+ Services

Deploying an assembly that is to be used with COM+ Services is only a little more involved than deploying any other .NET assembly.

The simplest way is to copy the assembly to the COM+ application's directory, known as dynamic registration . Doing this means that the assembly is not placed in the global assembly cache. However, there is a catch only a user who is a member of the Administrators group can register a serviced component. This means that with dynamic registration, the user of your application must be a member of the Administrators group.

The other way to register a serviced component is by manual registration administrator privileges are still required. Firstly, you have to provide the assembly with a strong name, and then register the assembly in the global assembly cache. We can use the gacutil utility to do this:

  gacutil -i AssemblyName.dll  

Registering of assemblies, the global assembly cache, and the use of strong names were discussed at length in Chapter 8.

The next step is to explicitly register the assembly with COM+ Services yourself, prior to any client program's execution. The program for doing this registration, RegSvcs.exe , is provided by Microsoft as part of the .NET SDK. When you run RegSvcs against a .NET component, it will create a COM+ application with the name specified by the ApplicationName attribute in the assembly and import the assembly into it.

click to expand

The typical syntax for RegSvcs is as follows :

  RegSvcs ComponentName [COM+AppName] [Typelibrary.tlb]  

With the second argument ( COM+AppName ), you can specify a different name for the COM+ application that will be created, and you can determine the name of the type library file that will be generated by providing a third argument ( TypeLibrary.tlb )

You may be wondering why RegSvcs.exe is necessary; as you may recall from the previous chapter on COM interoperability, .NET assemblies adhere to a different architecture from COM components. It's the job of RegSvcs.exe to resolve these discrepancies, so that .NET assemblies meet the interface expected by COM+ Services. To fulfill its job, RegSvcs.exe does four things:

  • It loads and registers the .NET assembly

  • It creates a type library for the .NET assembly

  • It imports the type library into a COM+ Services application

  • It uses metadata inside the DLL to properly configure the type library inside the COM+ Services application

Using Transactions with .NET Assemblies

There are two things that you have to do in order to equip a .NET class for transactions. Firstly, you have to modify the class with an attribute to indicate its level of transactional support. Secondly, you have to add code to the class to control its behavior when it participates in transactions. There is a way to tell COM+ how to automatically control its transactional behavior by using the [ AutoComplete ] attribute, covered a little later on.

Specifying Transactional Support

If you've used transactions from COM+ Services before, you may have seen the Transaction support setting on a class's Property window in the Component Services snap-in. This setting allows you to set the level of transactional support that COM+ Services will grant to a standard COM component.

In .NET, you can determine an assembly's level of transactional support differently, not by means of a graphical window in the Component Services snap-in, but programmatically, by means of the Transaction attribute defined in the System.EnterpriseServices namespace. For example, in the example below, we've specified that the following class should support transactions. Given this attribute value, our component will be configured to support transactions when it is imported into COM+ Services by RegSvcs.exe .

   [Transaction(TransactionOption.Supported)]     public class TxExample : ServicedComponent     {     }   

Supported is only one of five values that you can assign to a component's Transaction attribute, and they are listed in the TransactionOption enumeration, which is part of the System.EnterpriseServices namespace.

Attribute

Description

Supported

Objects of the class can enlist in the transactional context of its calling clients , if those calling clients did indeed begin a transaction . Such an object cannot instigate a transaction by itself.

Required

COM+ Services knows that objects of the class can only execute within the context of a transaction. If such an object is invoked by a client that has a transaction context, the object inherits the client's transaction context. If, however, the object is invoked by a client that does not have a transaction context, COM+ Services creates a context for the object.

RequiresNew

COM+ Services creates a brand new transaction for the class every time that it is invoked. Even if the object's client already has a transaction, COM+ Services creates a new one for the server object. As you might infer , classes configured in this way can only roll back their own transactions, and not the work of their clients.

Disabled

COM+ Services provides no transactional support for the class, even if such support is specified elsewhere in code. (In other words, calls that the class makes to ContextUtil to commit or roll back transactions are ignored. We'll see more about ContextUtil in the next section.)

NotSupported

The class does not enlist in transactions started by its clients; in other words, it's not placed in their context. When so configured, the objects of that class do not vote on whether the calling transaction is committed or rolled back.

In practice, most developers only use one or two of these settings. The Supported value is great for a class that will need to serve both transactional and non-transactional classes. For most other transactional classes in most situations, you can usually get away with designating the Required value. However, this is not to say that you will sometimes encounter situations in which one of the more complex values is needed; for further information, consult Professional Windows DNA , (Wrox Press, ISBN 1-861004-45-1).

Coding Transactions with ContextUtil

Modifying a class with the Transaction attribute is only part of what you have to do in order to enable it for transactions. You also have to specify how each method in that class will behave when invoked as part of a transaction. This is accomplished by means of the ContextUtil class of the System.EnterpriseServices namespace.

To put it simply, the ContextUtil class exposes a transaction's context. Once you have a reference to the transaction's context, you can explicitly cause that context to be committed or rolled back. The methods that you need to call for committing and rolling back transactions are exposed as static methods on the ContextUtil class, so you don't have to create an instance of the ContextUtil class in order to invoke them.

We will now move on to an example that makes use of ContextUtil , to demonstrate the use of COM+ transactions.

The example will simply attempt to update the UnitsOnOrder and UnitsInStock columns of Products table in the Northwind database however, the UnitsInStock column has a constraint that will prevent the value falling below 0, so any attempt to obtain non-existent stock would fail. Accordingly, the corresponding order placed will have to be withdrawn, achieved by rolling back the transaction.

First, we create a new Class Library called TransactionTest , add a reference to System.EnterpriseServices , and begin with the usual namespaces:

   using System;     using System.Data;     using System.Data.SqlClient;     using System.EnterpriseServices;     namespace Wrox.ProCSharp.ComPlus.TransactionTest     {   

And now we mark our class, TxTest , with the Required option of the Transaction attribute, enabling the class to use COM+ transactions.

   [Transaction(TransactionOption.Required)]     public class TxTest : ServicedComponent     {     public TxTest()     {     }   

The OrderProduct() method begins by defining a connection to the Northwind database.

       public bool OrderProduct(int ProductID, int Units)     {     SqlConnection conn = new SqlConnection("Data Source=(local);" +     "Initial Catalog=Northwind;User ID=sa;Password=");     try     {   

Next, we use the IsInTransaction property of ContextUtil to determine if we are currently participating in a transaction if we're not, then an exception is thrown.

   if (!ContextUtil.IsInTransaction)     throw new Exception("Not in transaction");   

The next few lines open the connection to the database, and attempt to update the relevant columns for the relevant products, throwing an exception if an insufficient number of rows is returned. Attempting to decrease the UnitsInStock column below 0 will result in an exception, due to the CK_UnitsInStock constraint on the Products table. Note that the UnitsOnOrder column is updated first; thus, if there is an error updating the UnitsInStock column, we definitely need to roll back any changes made.

   conn.Open();     SqlCommand cmd = new SqlCommand("UPDATE Products SET UnitsOnOrder = "+     "UnitsOnOrder + " + Units +     " WHERE ProductID = " + ProductID, conn);     int rowsAffected = cmd.ExecuteNonQuery();     if (rowsAffected != 1)     throw new Exception("Invalid number of rows affected");     cmd = new SqlCommand("UPDATE Products SET UnitsInStock = " +     "UnitsInStock - " + Units +     " WHERE ProductID = " + ProductID, conn);     rowsAffected = cmd.ExecuteNonQuery();     if (rowsAffected != 1)     throw new Exception("Invalid number of rows affected");   

At this point, there must be enough available stock since we have successfully updated both columns, and so we call ContextUtil 's SetComplete() method, effectively telling the DTC through the resource manager that, as far as it's concerned , the transaction needs to be committed.

   ContextUtil.SetComplete();     return true;     }   

In the catch block, we advise that there was an error, and invoke ContextUtil 's SetAbort() method. This method casts its vote for the cancellation of the transaction in which it is involved, and the DTC, after receiving this vote from the resource manager, will instruct each participant in the transaction to roll back any work attempted. We also return a value of false to indicate OrderProduct() 's failure.

   catch (Exception e)     {     Console.WriteLine("Transaction aborted with error: {0}", e.Message);     ContextUtil.SetAbort();     return false;     }   

Finally, we close any open connections in the finally block.

   finally     {     if (conn.State == ConnectionState.Open)     conn.Close();     }     }     }   

Remember, you don't have to create an instance of the ContextUtil object in order to invoke its SetComplete() and SetAbort() methods, since they are static methods.

After building the project, we install the assembly into the global assembly cache, and then register the assembly with COM+ Services by using regsvcs . A look in the Component Services window shows us that our service is indeed registered.

click to expand

We test the code with a simple Console Application client. Remember to add a reference to System.EnterpriseServices and the TransactionTest.dll

   using System;     using Wrox.ProCSharp.ComPlus.TransactionTest;     namespace Wrox.ProCSharp.ComPlus.TransactionTestClient     {     class Class1     {         [STAThread]     static void Main(string[] args)     {     TxTest txObj = new TxTest();     bool result = txObj.OrderProduct(1,20);     Console.WriteLine("OrderProduct returned {0} for 20 orders.", result);     result = txObj.OrderProduct(1,20);     Console.WriteLine("OrderProduct returned {0} for a further 20 "+     "orders.", result);     Console.ReadLine();     }     }     }   

The product in the Products table with a ProductId of 1 is Chai , and there are 39 units in stock. Thus we would expect the first call, OrderProduct(1,20) , to be successful, and the second attempt to fail.

click to expand

Most transaction-enabled code resembles the above example. It calls SetComplete() just before its exit point to commit all the work that has successfully been performed, or it calls SetAbort() in its error handler to roll everything back because of the error. Pretty easy, eh? There's another way that's even easier.

Microsoft provides a .NET attribute called AutoComplete . Methods modified with this attribute automatically apply the approach described above. Even though such methods never explicitly reference the ContextUtil class, they implicitly complete their transactions if they exit normally, or roll back all work if they exit due to an error (when an unhandled exception is thrown). Our OrderProduct() method above would look like this with the AutoComplete attribute applied:

   [AutoComplete]     public bool OrderProduct(int ProductID, int Units)     {       

Since the AutoComplete attribute is specified, this method call, when invoked, automatically votes in favor of the transaction committing if the method completes, or votes to abort the transaction if an unhandled exception is thrown. In our example above, we would need to call SetAbort() because our exceptions are handled.

Other Useful ContextUtil Methods and Properties

While we're on the subject of the ContextUtil class, let's look at a couple of other properties and methods that may prove useful to you in C# programming.

First, the IsCallerInRole() method provides for role-based security. As an input variable, this method accepts a string variable containing the name of a particular Windows 2000 security role. It returns a Boolean value indicating whether or not the user who is currently invoking the object is a member of the specified role.

In the code sample below, we've added a check to make sure that the user attempting to invoke OrderProduct() is an authorized member of Administrators role. If the user isn't in the role, OrderProduct() throws an exception.

   [Transaction(TransactionOption.Required)]     public class TxTest : ServicedComponent     ...     public bool OrderProduct(int ProductID, int Units)     {     if (!ContextUtil.IsCallerInRole("Administrators")     {     throw new Exception ("You are not authorized to place orders.");     }     // Continue Transaction Code here...     }   

A useful ContextUtil property that we've already seen is the IsInTransaction . IsInTransaction holds a Boolean value indicating whether the object is currently involved in a transaction.

As a professional C# programmer, you'll probably develop transactional components for use on a remote machine at remote installation that you do not control. To make sure that assemblies requiring transactional support are properly configured for it, you can make use of the IsInTransaction property of ContextUtil , and throw an error if this property is set to false . In our above example, you can test out this property by setting the TxTest class's attribute to TransactionOption.Disabled .

This completes our discussion of COM+ transactions and the ContextUtil class. Let's move on to object pooling.

Using Object Pooling with .NET Assemblies

It's not difficult to configure a .NET component for object pooling. Doing so entails modifying the class with an attribute.

The ObjectPooling Attribute

The attribute with which you should modify the class is ObjectPooling . This attribute receives four arguments:

  • The Enabled argument is first. It should be assigned a value of true

  • The MinPoolSize argument specifies the minimum number of object instances that COM+ Services should maintain in the class's object pool

  • The MaxPoolSize argument specifies the maximum number of object instances that COM+ Services should maintain in the class's object pool

  • The CreationTimeOut argument specifies the length of time (in milliseconds ) that COM+ Services should attempt to get an object from the pool before returning a failure

Here's an example of an ObjectPooling attribute with all four arguments applied to a class. We'll combine this snippet into a larger code sample near the end of this section.

   [ObjectPooling (Enabled=True, MinPoolSize=1, MaxPoolSize=100, CreationTimeout=30)]     public class CreditCard:ServicedComponent     {   

All serviced components must inherit from the ServicedComponent class. To use object pooling, there are three protected methods you have to override.

  • The CanBePooled() method is used by COM+ Services to ascertain whether the class can be pooled. This method should return a value of true to indicate that the class can be pooled. If the object cannot be pooled for any reason in its lifetime, such as an error situation from which it cannot gracefully recover, it should return false . This tells COM+ to discard the object, and create a new object to take its place.

  • The Activate() method is invoked by COM+ Services on a pooled object just before that object is handed to a new client. Endow this method with code for any initialization that the object should do between uses.

  • The Deactivate() method, Activate() 's counterpart , is fired by COM+ Services when the object is released by a client to return to the available pool.

The following code snippet shows a sample class configured for object pooling.

   [ObjectPooling (Enabled=true,     MinPoolSize=1,     MaxPoolSize=100,     CreationTimeout=30)]     public class ObjPoolTest : ServicedComponent     {     public ObjPoolTest()     {     // EXPENSIVE INTIALIZATION DONE HERE IF REQUIRED     // SINCE THE OBJECT IS POOLED, THIS WILL ONLY HAPPEN     // THE FIRST TIME THIS OBJECT IS CREATED     }     // THIS METHOD WOULD BE INVOKED     // BY COM SERVICES TO DETERMINE IF THE     // OBJECT IS POOLED.     protected override bool CanBePooled()     {     // YOU SHOULD RETURN true     // UNLESS THE OBJECT SHOULD NOT BE REUSED     return true;     }     // THIS METHOD WOULD BE INVOKED     // BY COM SERVICES WHEN THE OBJECT     // IS BEING GIVEN TO A CLIENT.     protected override void Activate()     {     //INITIALIZATION CODE WOULD GO HERE.     }         // THIS METHOD WOULD BE INVOKED     // BY COM SERVICES WHEN THE OBJECT IS     // BEING RETURNED TO THE POOL.     protected override void Deactivate()     {     // TERMINATION CODE WOULD GO HERE.     }         //THIS METHOD WOULD BE INVOKED BY THE CLIENT.     public bool OrderProduct(int ProductID, int Units)     {     // CODE FOR ORDERING A PRODUCT     }     }   

Using JIT Activation with .NET Assemblies

To configure a .NET class for JIT activation, you merely modify the class with the JustInTimeActivation attribute, providing a value of true . Here, we present a simple example that demonstrates the activation and de-activation of an object.

We simply override the Activate() and Deactivate() methods to indicate when our object is activated and deactivated.

   [JustInTimeActivation(true)]     public class JitTest : ServicedComponent     {     public JitTest()     {     }     protected override void Activate()     {     Console.WriteLine("Activated!!!");     }         protected override void Deactivate()     {     Console.WriteLine("De-Activated!!!");     }   

We have a very simple method, DoSomething() . The only thing of note here is the DeactivateOnReturn property of ContextUtil . By setting this to true , we are setting the done bit of the COM+ context ; this bit is inspected by COM+ when a method call finishes, and the object will be deactivated if this bit is set.

   public int number = 0 ;         public void DoSomething()     {     Console.WriteLine("This is our JIT object in action - " +     "Attempt {0}.",number);     number++;     ContextUtil.DeactivateOnReturn = true;     }     }   

The attempt to increase number here also demonstrates the importance of using stateless objects with Just-In-Time activation. The object is deactivated at the end of the DoSomething() method, and any state the object has is removed from memory. The next method call to our object will activate it, but all its properties will have been re- initialized . You can witness this all with a simple test client like the following:

   Console.WriteLine("There is no JIT object yet....");     JitTest jitObj = new JitTest();     Console.WriteLine("Now we call a method on our JIT object.");     jitObj.DoSomething();     Console.WriteLine("We have returned from the method....");     jitObj.DoSomething();   

Note how the count is not incremented between method invocations:

click to expand

Delving deeper into COM+ Services becomes more involved than we have room for here we have only been able to scratch the surface with our transaction example, and discussion of object pooling and Just-In-Time activation.

To further explore these COM+ Services, and the use of Messaging Services and Queued Components in .NET, we refer the reader to Data-Centric .NET Programming with C# (Wrox Press, ISBN 1-861005-92-x).

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net