Chapter 8. COM-Interop and Enterprise Services Gotchas

COM-Interop in .NET allows a .NET component to communicate with a COM component using a Runtime Callable Wrapper. It also makes it possible for unmanaged code to talk to a .NET component as if it’s a COM component by way of a COM Callable Wrapper. These facilities and the related API make it easier to interoperate. However, you need to give due consideration to the object life cycle and threading issues to get the most out of interoperability. Furthermore, the quickest way to expose a .NET object for COM interoperability is to set the project settings to Register for COM interop. While this may be the “it’s that simple” approach, you need to watch out for a number of things. In this chapter I focus on details you should be aware of to make interoperability work for you. I also delve into issues related to Enterprise Services, a set of classes that allows you to programmatically utilize the COM+ services in the .NET Framework.

I assume that you are fairly familiar with COM [Box98]. The discussions in this chapter are intended for programmers with COM knowledge (aCOMplished programmers?!) and interested in .NET to COM interoperability. The gotchas are organized as follows:

  • Gotchas 65-70 discuss issues with using COM components in .NET.

  • Gotchas 71-73 are about using .NET classes from unmanaged code through COM.

  • Gotchas 74-75 deal with Enterprise Services.

GOTCHA #65 Release of COM object is confusing

Programming with COM in C++ has always been quite a task. You can recognize C++ COM programmers by the scars on their bodies. By comparison, VB6 COM programming is easier. I have heard that one of Microsoft’s goals for .NET is to make COM interoperability as simple as possible. Unfortunately, they have reached something of a middle ground, and even VB.NET programmers have to do some extra work now to clean up COM objects.

In C++, you have to call Release() on an interface pointer when you are done with the COM object. Forgetting to call Release() leaves the COM component stuck in memory. In VB6, setting the reference to Nothing is sufficient; the call to Release() is done automatically.

In .NET, you use a Runtime Callable Wrapper (RCW) to communicate with the COM component. As you would guess, the RCW takes care of releasing the component when it is cleaned up. However, you don’t have much control over the timing of the garbage collection. So, the logical thought is to Dispose() the RCW when you’re done with it. Sorry, the RCW doesn’t implement the IDisposable interface. It would be easier if it did, wouldn’t it? So what should you do?

Suppose you have a COM component (not shown here) which pops up a message box (just for demo purposes) when an object is created and also when it’s destroyed. Look at the .NET code that communicates with this component in Example 8-1.

Example 8-1. Problem releasing the COM object

C# (ThouShaltReleaseObject)

using System;

namespace COMCompUser
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            Console.WriteLine("Creating object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            Console.WriteLine("Calling Method1");
            theMyComp.Method1();

            //Console.WriteLine("Releasing the object");
            //System.Runtime.InteropServices.Marshal.ReleaseComObject(
            //    theMyComp);

            Console.WriteLine("Setting reference to null");
            theMyComp = null;

            Console.WriteLine("Has the Object been destroyed?");
            Console.ReadLine();
        }
    }
}

VB.NET (ThouShaltReleaseObject)

Module Test

    Sub Main()
        Console.WriteLine("Creating object")
        Dim theMyComp As MyCOMCompLib.IMyComp _
            = New MyCOMCompLib.MyCompClass

        Console.WriteLine("Calling Method1")
        theMyComp.Method1()

        'Console.WriteLine("Releasing the object")
        'System.Runtime.InteropServices.Marshal.ReleaseComObject( _
        '    theMyComp)

        Console.WriteLine("Setting reference to null")
        theMyComp = Nothing

        Console.WriteLine("Has the Object been destroyed?")
        Console.ReadLine()
    End Sub

End Module

In this example you create an RCW for the COM component. You call Method1() on it, then set the reference to the RCW to null/Nothing. As the above code executes, you get the output shown in Figures 8-1 through 8-3.

Object created

Figure 8-1. Object created

The object is created (Figure 8-1) and Method1() is called (Figure 8-2). However, the object has not been destroyed yet (Figure 8-3). Pressing Return now, in response to Console.ReadLine(), will pop up the message box showing that the object has been destroyed. As you can see, setting the reference to null/Nothing does not release the object, a departure from COM component interaction in VB6.

Calling Method1()

Figure 8-2. Calling Method1()

Has the object been destroyed?

Figure 8-3. Has the object been destroyed?

How can you clean up the object? As I mentioned, there is no Dispose() method you can call using the RCW reference. To properly release the object in Example 8-1, uncomment the two commented-out statements in the Main() method. This gives you:

    Console.WriteLine("Releasing the object");
    System.Runtime.InteropServices.Marshal.ReleaseComObject(
        theMyComp);

When you are done using the component, you invoke the System.Runtime.InteropServices.Marshal.ReleaseComObject() static/Shared method. This releases the COM component at that very moment without waiting for the garbage collector. (It’s better to call ReleaseComObject() from a finally block to make sure the object is released even if an exception occurs.)

Should you call ReleaseComObject() as soon as you’re done using a COM component? Well, if you don’t call it, the object is held until the garbage collector eventually (and much later than you might desire), decides to clean it up. The program could end up holding critical resources for an extended period of time, and this might also affect the overall performance of the system.

However, calling ReleaseComObject() has risks. (The next gotcha discusses one of them.) If you are not worried about resources being held for too long, then do not bother using ReleaseComObject(). This is especially true for client applications. If, on the other hand, you are concerned about out-of-process unmanaged resources being held (especially in a server application), then you need to use ReleaseComObject().

For insight into other problems with ReleaseComObject(), see the blogs referenced in "ReleaseComObject() issues" in the "On the Web" section of the Appendix.

IN A NUTSHELL

When you are done using a COM component, if you want any critical resources to be cleaned up right away, release it using the ReleaseComObject() static/Shared method of the System.Runtime.InteropServices.Marshal class. Do so in a finally block. Note the risks with ReleaseComObject(), however.

GOTCHA #66 Using interface pointers after calling ReleaseComObject() will fail

As I mentioned in Gotcha #65, "Release of COM object is confusing,” there are a few problems with using the ReleaseComObject() method. In this gotcha I discuss one of those issues—dealing with multiple interfaces.

When working with COM components you often use more than one interface. For instance, say you have a component that exposes two interfaces, IMyComp and IMyComp2. When using these two interfaces, which one should you release? Those of you who have worked with COM in C++ or VB6 are probably saying, “Both of them, of course.” But in .NET, that is not the case. You can’t continue to use a COM component after you have called ReleaseComObject() on any of the interface references to that component.

Let’s pursue an example where a COM component supports the two interfaces IMyComp and IMyComp2, with one method in each, Method1() and Method2(). Example 8-2 shows the .NET code to access it.

Example 8-2. Working with multiple interfaces of a COM component

C# (COMInterfaces)

using System;

namespace COMCompUser
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            Console.WriteLine("Creating object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            MyCOMCompLib.IMyComp2 theMyComp2
                = (MyCOMCompLib.IMyComp2) theMyComp;

            Console.WriteLine("Calling Method1");
            theMyComp.Method1();

            Console.WriteLine("Releasing the object");
            System.Runtime.InteropServices.Marshal.ReleaseComObject(theMyComp);

            Console.WriteLine("Calling Method2");
            theMyComp2.Method2();
            Console.ReadLine();
        }
    }
}

VB.NET (COMInterfaces)

Module Test

    Sub Main()
        Console.WriteLine("Creating object")
        Dim theMyComp As MyCOMCompLib.IMyComp _
            = New MyCOMCompLib.MyCompClass

        Dim theMyComp2 As MyCOMCompLib.IMyComp2 _
            = CType(theMyComp, MyCOMCompLib.IMyComp2)

        Console.WriteLine("Calling Method1")
        theMyComp.Method1()

        Console.WriteLine("Releasing the object")
            System.Runtime.InteropServices.Marshal.ReleaseComObject( _
                theMyComp)

        Console.WriteLine("Calling Method2")
        theMyComp2.Method2()
        Console.ReadLine()
    End Sub

End Module

In this example, you obtain two references to the COM component, one for each of its interfaces. You then release the first reference using ReleaseComObject(). When you invoke Method2() using the IMyComp2 interface reference, you get a NullReferenceException, as shown in Figure 8-4.

Output from Example 8-2

Figure 8-4. Output from Example 8-2

The exception is raised from the RCW because, as its name implies, ReleaseComObject() has released the object. Working with a COM component using the interface reference in .NET is different from C++ and VB6.

Understand the issues related to proper cleanup and the state of the RCW. This is another reason to isolate the interaction with the component as discussed in Gotcha #70, "Spattering access to COM components makes code hard to maintain.”

IN A NUTSHELL

If you have obtained multiple interfaces on a COM component, do not use any of the references after calling ReleaseComObject. The RCW disconnects from the COM object when you call ReleaseComObject(). Understand that calling ReleaseComObject() is not the same as calling IUnknown’s Release() method.

GOTCHA #67 Cross-apartment calls are expensive

One of the goals of COM is to make it easier to substitute one component for another. You should be able to swap out a component that supports a set of interfaces and replace it with another one that supports the same set of interfaces and abides by their implied contract. The client must be able to interchange multiple components that satisfy a set of interface contracts, and interact with all of them in the same way.

What is the purpose of an apartment [Box98]? It is to make the component substitutable while having different threading needs. Say a component A takes care of thread safety. It is meant to be invoked from multiple threads simultaneously. Say another component B, which fulfills the same interface contract as component A, doesn’t deal with thread safety. It is meant to be invoked from a single thread. How can a client swap these two components seamlessly?

The purpose of an apartment is to provide a logical thread isolation boundary in COM. A client thread executes in an apartment. The object may be created in the same or a different apartment. If the object is in the same apartment as the client, the calls to its methods are executed directly by the client thread. If they are in different apartments, then the client communicates using a proxy, and a method call request is sent to the other apartment. One of the threads in the other apartment picks up the request and executes it. An object that is not thread-safe is created in a Single Threaded Apartment (STA) while an object that can handle multiple threads is created in a Multithreaded Apartment (MTA) . Once the apartment in which a thread will execute is decided, it is pretty much set.

While multiple threads may execute in an MTA, only one thread ever executes in an STA. What is the effect of multiple threads invoking a method on an object at the same time? If the object is in an STA, then the method calls are queued and the single thread that resides in the object’s STA picks one call at a time and executes it. This guarantees that only one thread calls the object’s methods at any time. If the object, however, is in an MTA, then multiple threads in the MTA will pick up requests from the queue and execute them simultaneously. Of course, if any of the calling threads are in the same apartment as the object, then the call will be direct and not go through a proxy, stub, and call message queue.

So, now that I have reviewed what apartments in COM are, what is the significance of this when it comes to .NET code that interacts with a COM component? If the component and client are in the same apartment, you have no COM overhead. You incur only the marshalling overhead (to go from .NET through an RCW to COM). However, if the client and the component are in different apartments, then you incur overhead at both levels. This is shown in Figure 8-5 and demonstrated in Example 8-3.

Overhead associated with cross-apartment invocation

Figure 8-5. Overhead associated with cross-apartment invocation

Example 8-3. Effect of apartment-related attributes

C# (Apartment )

using System;

namespace COMCompUser
{
    class Test
    {
        [STAThread] // You will see the effect of this attribute
        static void Main(string[] args)
        {
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            theMyComp.Method1();
        }
    }
}

VB.NET (Apartment)

Module Test

    'You will see the effect of this attribute
    <STAThread()> _
    Sub Main()
        Dim theMyComp as MyCOMCompLib.IMyComp _
                = new MyCOMCompLib.MyCompClass()

        theMyComp.Method1()
    End Sub

End Module

In this example, you create a COM component (actually the RCW of the COM component) and call Method1() on it. You place the STAThread attribute on the Main() method. Now, let’s put a breakpoint in the COM component’s Method1() and look at the call stack. The stack trace is shown in Figure 8-6.

Stack trace of Method1 of COM component in Example 8-3

Figure 8-6. Stack trace of Method1 of COM component in Example 8-3

You are only two levels deep in the call stack in this case. Now, change the STAThread attribute in the Main() method to MTAThread, as shown in Example 8-4, and rerun the program.

Example 8-4. Changing the apartment of Main() method

C# (Apartment )

        //...
        [MTAThread] // You will see the effect of this attribute
        static void Main(string[] args)
        //...

VB.NET (Apartment)

    '...
    'You will see the effect of this attribute
    <MTAThread()> _
    Sub Main()
        '...

Figure 8-7 shows the new call stack (or as much of it as will fit).

The call stack is 59 levels deep. What made the difference? The client code is running in an MTA while the component resides in a STA. The call to the method now has to go through a proxy and a stub. It is better to avoid this overhead when invoking COM components. While in principle COM isolates the threading needs of a component from its client, you still need to be sensitive to the impact on performance.

So how do you interact efficiently with two components that have different threading models? You might consider creating two different threads in your .NET applica

COM component’s Method1() call stack for Example 8-4

Figure 8-7. COM component’s Method1() call stack for Example 8-4

tion, one in an STA and the other in an MTA, then interact with the COM components from the appropriate threads, based on the COM component’s apartment. This is another reason to isolate the interaction with the component as discussed in Gotcha #70, "Spattering access to COM components makes code hard to maintain.” You can find the apartment of the COM object by looking at the ThreadingModel in its Registry settings, or by reading its documentation. Details of setting the apartment of a thread are presented later in Example 8-7.

IN A NUTSHELL

Understand the apartment of your thread to get the best performance when interacting with COM components. If possible, invoke methods on a COM component from the same apartment as the component.

GOTCHA #68 Default apartment of main thread is inconsistent across languages

In Example 8-3 you first marked the Main() method with the STAThread attribute, then in Example 8-4 changed it to MTAThread. What is the apartment if you do not mark Main() with either attribute? The answer, unfortunately, depends on which language you are using. Consider Example 8-5.

Example 8-5. Default apartment

C# (DefaultApartment)

using System;
using System.Threading;

namespace COMCompUser
{
    class Test
    {
        static void Worker()
        {
            Thread currentThread = Thread.CurrentThread;
            Console.WriteLine("In worker thread");

            Console.WriteLine("Apartment in worker is {0}",
                currentThread.ApartmentState.ToString());

            Console.WriteLine("Creating COM object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            Console.WriteLine("Apartment in worker is {0}",
                currentThread.ApartmentState.ToString());
        }

        static void Main(string[] args)
        {
            Thread currentThread = Thread.CurrentThread;
            Console.WriteLine("Apartment in main is {0}",
                currentThread.ApartmentState.ToString());

            Console.WriteLine("Creating COM object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            Console.WriteLine("Apartment in main is {0}",
                currentThread.ApartmentState.ToString());

            Thread workerThread = new Thread(
                new ThreadStart(Worker));
            //Not setting IsBackground on thread intentionally
            workerThread.Start();
        }
    }
}

VB.NET (DefaultApartment)

Imports System.Threading

Module Test

    Private Sub Worker()
        Dim currentThread As Thread = Thread.CurrentThread
        Console.WriteLine("In worker thread")

        Console.WriteLine("Apartment in worker is {0}", _
                 currentThread.ApartmentState.ToString())

        Console.WriteLine("Creating COM object")
        Dim theMyComp as MyCOMCompLib.IMyComp _
                = new MyCOMCompLib.MyCompClass()

        Console.WriteLine("Apartment in worker is {0}", _
         currentThread.ApartmentState.ToString())
    End Sub

    Public Sub Main()
        Dim currentThread As Thread = Thread.CurrentThread

        Console.WriteLine("Apartment in main is {0}", _
         currentThread.ApartmentState.ToString())

        Console.WriteLine("Creating COM object")
        Dim theMyComp as MyCOMCompLib.IMyComp _
                = new MyCOMCompLib.MyCompClass()

        Console.WriteLine("Apartment in main is {0}", _
         currentThread.ApartmentState.ToString())

        Dim workerThread As New Thread(AddressOf Worker)
        'Not setting IsBackground on thread intentionally
        workerThread.Start()
    End Sub
End Module

In this example, the apartment attribute is not set on the Main() and Worker() methods. When you execute the code, the output differs between the languages. The output from the C# version is shown in Figure 8-8; the VB.NET output appears in Figure 8-9.

Output from the C# version of Example 8-5

Figure 8-8. Output from the C# version of Example 8-5

Output from the VB.NET version of Example 8-5

Figure 8-9. Output from the VB.NET version of Example 8-5

As you can see, the apartment of Main() is different between the C# and VB.NET versions. This is because in C#, if you don’t set either STAThread or MTAThread, the apartment is first unknown. Once you access the COM component, it is set to MTA. In VB.NET, however, if you don’t set either of the attributes, the compiler sets the STAThread automatically for Main(). (You’ll see this if you view the MSIL for Main() with ildasm.exe.) But the apartment of the worker thread, i.e., the apartment within the Worker() method, is MTA for both languages. So the apartment of the Main() thread defaults to different values depending on the language used. This can be troublesome to programmers who have to maintain code in both languages.

IN A NUTSHELL

Understand the apartment in which your thread is running. Don’t assume or depend on the defaults. Set it explicitly to the desired value.

GOTCHA #69 STAThread attribute may have no effect on your methods

Let’s continue with the discussions from Gotcha #68, "Default apartment of main thread is inconsistent across languages" You may think, “Well, I want my worker thread to be in an STA, so let me mark the method with the STAThread attribute just like it’s done in Main().” Is that a good idea? Let’s find out. Consider Example 8-6.

Example 8-6. Effect of using the STAThreadAttribute

C# (SettingApartment)

using System;
using System.Threading;

namespace COMCompUser
{
    class Test
    {
        [STAThread]
        static void Worker()
        {
            Thread currentThread = Thread.CurrentThread;
            Console.WriteLine("In worker thread");

            Console.WriteLine("Apartment in worker is {0}",
                currentThread.ApartmentState.ToString());

            Console.WriteLine("Creating COM object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            Console.WriteLine("Apartment in worker is {0}",
                currentThread.ApartmentState.ToString());
        }

        [STAThread]
        static void Main(string[] args)
        {
            Thread currentThread = Thread.CurrentThread;
            Console.WriteLine("Apartment in main is {0}",
                currentThread.ApartmentState.ToString());

            Console.WriteLine("Creating COM object");
            MyCOMCompLib.IMyComp theMyComp
                = new MyCOMCompLib.MyCompClass();

            Console.WriteLine("Apartment in main is {0}",
                currentThread.ApartmentState.ToString());

            Thread workerThread = new Thread(
                new ThreadStart(Worker));
            //Not setting IsBackground on thread intentionally
            workerThread.Start();
        }
    }
}

VB.NET (SettingApartment)

Imports System.Threading

Module Test

    <STAThread()> _
    Private Sub Worker()

        Dim currentThread As Thread = Thread.CurrentThread

        Console.WriteLine("In worker thread")


        Console.WriteLine("Apartment in worker is {0}", _
                     currentThread.ApartmentState.ToString())

        Console.WriteLine("Creating COM object")
        Dim theMyComp As MyCOMCompLib.IMyComp _
         = New MyCOMCompLib.MyCompClass

        Console.WriteLine("Apartment in worker is {0}", _
         currentThread.ApartmentState.ToString())
    End Sub

    <STAThread()> _
    Public Sub Main()

        Dim currentThread As Thread = Thread.CurrentThread
        Console.WriteLine("Apartment in main is {0}", _
         currentThread.ApartmentState.ToString())

        Console.WriteLine("Creating COM object")

        dim theMyComp as MyCOMCompLib.IMyComp _
                = new MyCOMCompLib.MyCompClass()

        Console.WriteLine("Apartment in main is {0}", _
         currentThread.ApartmentState.ToString())

        Dim workerThread As New Thread(AddressOf Worker)

        'Not setting IsBackground on thread intentionally
        workerThread.Start()
    End Sub

End Module

In this example, you have set the STAThread attribute on the Worker() method, which is called from a separate thread. If you expect the apartment of the thread within the Worker() method to be STA, you’re in for a surprise. The output from the program is shown in Figure 8-10.

Output from Example 8-6

Figure 8-10. Output from Example 8-6

Even though you have set the STAThread attribute on the Worker() method, it is running in an MTA thread. How do you make Worker() run in an STA thread? The code that does that is shown in Example 8-7.

Example 8-7. Correct way to set apartment for a thread

C# (SettingApartment)

    //...
    class Test
    {
        //[STAThread]
        static void Worker()
        {
            //...
        }

        [STAThread]
        static void Main(string[] args)
        {
            //...

            Thread workerThread = new Thread(
                new ThreadStart(Worker));
            //Not setting IsBackground on thread intentionally

            workerThread.ApartmentState = ApartmentState.STA;

            workerThread.Start();
        }
    }
}

VB.NET (SettingApartment)

'...
Module Test

    '<STAThread()> _
    Private Sub Worker()
        '...

    End Sub

    <STAThread()> _
    Public Sub Main()

        '...

        Dim workerThread As New Thread(AddressOf Worker)

        workerThread.ApartmentState = ApartmentState.STA

        'Not setting IsBackground on thread intentionally
        workerThread.Start()
    End Sub

End Module

As soon as you create the thread, set the ApartmentState property on it. Don’t set the STAThread attribute (or the MTAThread attribute) on methods like the Worker() method. The output from this modified version is shown in Figure 8-11.

Output from Example 8-7

Figure 8-11. Output from Example 8-7

IN A NUTSHELL

For threads that interact with COM components, set the apartment of the thread yourself; don’t use method attributes to do so.

GOTCHA #70 Spattering access to COM components makes code hard to maintain

After reviewing the gotchas in communicating from .NET to COM, you might sit back with a deep breath and say, “Hmm, what a mess. How am I going to use all these correctly in my application?”

Well, let’s start with how you use them incorrectly. The easiest way to fail at it is to call COM components from all over your code, as shown in Figure 8-12 (this diagram shows classes and not instances; instances of your .NET classes will talk to different instances of RCW during execution). In this figure, a number of .NET classes want to utilize a COM component. Each one of them creates an instance of RCW and interacts with it. The complexity of the application goes up in this approach for reasons mentioned below.

There are several things that you need to do when interacting with COM:

  • Decide if you must release COM components when you no longer need them, or if you’re content to let garbage collection take care of them.

  • Communicate with COM components from the proper apartment.

Improper use of COM component in an application

Figure 8-12. Improper use of COM component in an application

  • Set the apartment of your .NET threads correctly.

  • Don’t use any interface on the COM component once you have called ReleaseComObject() on it.

How can you verify that your application follows these guidelines?

One easy way to manage this complexity is to isolate access to COM components in logical wrapper classes, as shown in Figure 8-13. First, build a .NET class that exposes the intended interface(s) of the COM component to the rest of the application. From within this wrapper class, you will interact with the COM Component(s). You can carefully manage the access and lifetime of the component(s) based on their apartment, state, and resource utilization. For example, by implementing the IDisposable interface and calling ReleaseComObject() in the Dispose() method, you can take care of proper cleanup of the COM component. Likewise, you can determine the apartment needs of the COM component and launch a thread with proper apartment settings to interact with it if required. The rest of your application does not have to worry about these details.

Isolation of access to COM components in an application

Figure 8-13. Isolation of access to COM components in an application

This isolation confers several benefits:

  • It reduces the coupling of your code to the COM component.

  • You can better manage the lifetime of the COM component.

  • You can tailor the performance to take advantage of specific threading needs based on the apartment of the COM component.

  • The rest of your system does not have to deal with the complexity of correctly interacting with COM components.

  • You can focus on supporting the functionality of your application and map its implementation to the appropriate COM components.

  • You can replace or substitute the COM component easily without affecting the rest of your application.

  • In the future, if so desired, you can trade the COM component for an alternate .NET implementation with minimal impact on the code.

IN A NUTSHELL

Route all interaction to the RCW of a COM component through a .NET wrapper class. This isolates the problems related to interoperability and makes your code easier to maintain.

GOTCHA #71 Auto-generating GUID for your classes leads to versioning woes

Let’s switch focus to accessing your .NET classes from unmanaged code using COM.

How can you do that? You can use COM to achieve this—Microsoft has done something smart here. You can associate a GUID with a .NET component and register its assembly as if it were a COM component. In the Registry, the name and location of the assembly are stored under the key

    HKEY_CLASSES_ROOTCLSID{...GUID...}InprocServer32

(where ...GUID... is the GUID given to the .NET class) as shown in Figure 8-14.

The InprocServer32’s Default key entry refers to the host component DLL as mscoree.dll, the primary DLL that initializes the CLR. Remember that when a

Registration of .NET class for access from COM

Figure 8-14. Registration of .NET class for access from COM

client activates a COM object, the component’s DLL is loaded and the DllGetClassObject() method is invoked with the CLSID/GUID for the component as its parameter (again refer to [Box98]). If the CLSID/GUID is related to a .NET class, the component DLL that’s loaded is the CLR’s mscoree.dll. It then finds the name of the .NET class and the location of the assembly from the Class and CodeBase entries in the Registry key. The CLR then creates a COM Callable Wrapper (CCW) object as a proxy for the client to interact with. This CCW manages and properly routes the calls from the COM client to the .NET component. So.NET classes can easily pretend to be COM components.

You know that every COM component has a Class ID or CLSID that is a Globally Unique ID (GUID). In exposing a .NET class for COM interoperability, if you do not specify a GUID attribute for the class, then a new GUID is created. According to the documentation, the GUID that is automatically generated uniquely identifies the combination of the namespace name and the class name. This is to guarantee that no two classes will have the same GUID.

In practice though, the generated GUID is unique to the assembly version as well; i.e., the GUID generation scheme uses the namespace name, the class name, and the assembly version number. This may actually be good in a lot of ways. However, what if you change nothing but the revision number part of the four-part version number, and the newer version is compatible with the older version? Say your intent is for clients that use the older version to use this newer version, which has a different revision number, maybe because of a small bug fix. Consider Example 8-8.

Tip

By default, the assembly version in your project is set to 1.0.*. This ends up generating a new version number each time you build your project. That’s trouble from the point of view of GUID generation. Give a proper assembly version number for your projects.

Example 8-8. Problem with automatic GUID generation

C# (GUID)

//MyComponent.cs
using System;

namespace ALib
{
    public class MyComponent
    {
        public void Method1()
        {
            //... whatever code goes here
        }
    }
}

//AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;

//... (not shown)

//
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.0")]

VB.NET (GUID)

'MyComponent.vb

Public Class MyComponent
    Public Sub Method1()
        '... whatever code goes here
    End Sub
End Class


'AssemblyInfo.vb
Imports System
Imports System.Reflection
Imports System.Runtime.InteropServices

' ... (Not shown)

' Version information for an assembly consists of the following four values:
'
'      Major Version
'      Minor Version
'      Build Number
'      Revision
'
' You can specify all the values or you can default the Build and Revision Numbers
' by using the '*' as shown below:

<Assembly: AssemblyVersion("1.0.0.0")>

For the above assembly, the version number is 1.0.0.0. You have selected Register for COM Interop in the project settings. Part of the registration information created from this assembly is shown in Example 8-9.

Example 8-9. COM Registration information for .NET component

REGEDIT4

[HKEY_CLASSES_ROOTALib.MyComponent]
@="ALib.MyComponent"

[HKEY_CLASSES_ROOTALib.MyComponentCLSID]
@="{4F59B9B0-40ED-3CEE-B560-56CEECDB9F65}"

[HKEY_CLASSES_ROOTCLSID{4F59B9B0-40ED-3CEE-B560-56CEECDB9F65}]
@="ALib.MyComponent"

[HKEY_CLASSES_ROOTCLSID{4F59B9B0-40ED-3CEE-B560-56CEECDB9F65}InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="ALib.MyComponent"
"Assembly"="ALib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v1.1.4322"
...

The GUID created for class MyComponent is 4F59B9B0-40ED-3CEE-B560-56CEECDB9F65. Now let’s modify the version number in the AssemblyInfo file from 1.0.0.0 to 1.0.0.1 (changing only the revision number).

When you recompile and regenerate the registration, you will find the information shown in Example 8-10.

Example 8-10. Effect of change in version number on generated GUID

REGEDIT4

[HKEY_CLASSES_ROOTALib.MyComponent]
@="ALib.MyComponent"

[HKEY_CLASSES_ROOTALib.MyComponentCLSID]
@="{D0190E9A-AD60-3EA2-B268-2C5D96507F21}"

[HKEY_CLASSES_ROOTCLSID{D0190E9A-AD60-3EA2-B268-2C5D96507F21}]
@="ALib.MyComponent"

[HKEY_CLASSES_ROOTCLSID{D0190E9A-AD60-3EA2-B268-2C5D96507F21}InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="ALib.MyComponent"
"Assembly"="ALib, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v1.1.4322"
...

The GUID value is now D0190E9A-AD60-3EA2-B268-2C5D96507F21, which is different from the one generated earlier. This might not be desirable if you have changed only the revision number and still want existing COM clients to use this updated component, because they won’t be able to. The intent of changing the revision number may be a minor bug fix, or a very small change. The disadvantage of generating the GUID automatically is that it becomes harder to manage the versioning of components. This problem can be easily avoided by providing the GUID attribute for classes you want to expose for COM interoperability, as shown in Example 8-11.

Example 8-11. Exposing .NET component using GUID attribute

C# (ClassInterfaceType )

using System;
using System.Runtime.InteropServices;

namespace ALib
{
    [Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"),
        ClassInterface(ClassInterfaceType.AutoDispatch)]
    public class MyComponent
    {
        public void Method1()
        {
            //...
        }
    }
}

VB.NET (ClassInterfaceType)

Imports System.Runtime.InteropServices

<Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"), _
  ClassInterface(ClassInterfaceType.AutoDispatch
               )> _
Public Class MyComponent
    Public Sub Method1()
        '...
    End Sub
End Class

By default, all public classes are exposed for COM interoperability. You must set the COMVisible attribute on the class (or the assembly to affect all classes in it) to false if you don’t want to expose the class(es).

IN A NUTSHELL

Set the GUID attribute on your class if you intend to make it available for COM interoperability. To prevent COM access, use the COMVisible(false) attribute.

GOTCHA #72 All but one of the ClassInterface options are ineffective

COM clients interact with a component through its supported interfaces. There are three ways to expose a .NET class for COM interoperability. The options are specified through the ClassInterfaceType enumeration’s values of AutoDispatch, AutoDual, and None. What is the consequence of choosing one option over another? Understand this to avoid some common mistakes in enabling COM interoperability for your .NET classes. In this gotcha I will discuss each of these approaches. First, consider using AutoDispatch, as shown in Example 8-12 (which is the same as Example 8-11 but has different lines highlighted).

Example 8-12. Exposing .NET component using AutoDispatch

C# (ClassInterfaceType)

using System;
using System.Runtime.InteropServices;

namespace ALib
{
    [Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"),
        ClassInterface(ClassInterfaceType.AutoDispatch)]
    public class MyComponent
    {
        public void Method1()
        {
            //...
        }
    }
}

VB.NET (ClassInterfaceType)

Imports System.Runtime.InteropServices

<Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"), _
  ClassInterface(ClassInterfaceType.AutoDispatch)> _
Public Class MyComponent
    Public Sub Method1()
        '...
    End Sub
End Class

In this case, you have set the ClassInterface attribute on the class to ClassInterfaceType.AutoDispatch, which is the default value for that attribute. What is the consequence of this setting? What’s exposed for the client is affected by this setting and is shown in Figure 8-15.

Effect of ClassInterfaceType.AutoDispatch

Figure 8-15. Effect of ClassInterfaceType.AutoDispatch

The class implements only the IDispatch interface. The methods of the class are not exposed directly, so the only way for a client to access this component is using automation. While this is great for scripting clients and VB6 using automation, it is not desirable from the point of view of C++ clients and VB6 in early binding mode.

In the second approach, let’s modify the code to use AutoDual as in Example 8-13. The change from AutoDispatch to AutoDual results in a change to what the client sees, as shown in Figure 8-16.

Example 8-13. Exposing .NET component using AutoDual

C# (ClassInterfaceType )

using System;
using System.Runtime.InteropServices;

namespace ALib
{
    [Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"),
        ClassInterface(ClassInterfaceType.AutoDual)]
    public class MyComponent
    {
        public void Method1()
        {
            //...
        }
    }
}

VB.NET (ClassInterfaceType)

Imports System.Runtime.InteropServices

<Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"), _
  ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class MyComponent
    Public Sub Method1()
        '...
    End Sub
End Class

There is good news and bad news. The good news is that you are exposing Method1() of the component for clients to use. This means that strongly typed languages such as C++ can use your component easily. However, in addition to Method1(), methods of the Object base class are exposed. Second, remember that in COM, interfaces are immutable. Unfortunately, if you add methods, or just move them around, the interface generated will be different. This violates interface immutability, and forces client applications to be recompiled.

A third approach, and the most effective one, is to have the class expose no COM interface at all. Rather, it exposes .NET interfaces separately to COM clients. This is shown in Example 8-14.

Effect of ClassInterfaceType.AutoDual

Figure 8-16. Effect of ClassInterfaceType.AutoDual

Example 8-14. The only option that works

C# (ClassInterfaceType)

using System;
using System.Runtime.InteropServices;

namespace ALib
{
    [Guid("74555C62-75CD-4c87-940A-4AE8A69FFCB2"),
        InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IMy
    {
        void Method1();
    }

    [Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"),
        ClassInterface(ClassInterfaceType.None)]
    public class MyComponent : IMy
    {
        public void Method1()
        {
            //...
        }
    }
}

VB.NET (ClassInterfaceType)

Imports System.Runtime.InteropServices

<Guid("74555C62-75CD-4c87-940A-4AE8A69FFCB2"), _
  InterfaceType(ComInterfaceType.InterfaceIsDual)> _
Public Interface IMy
    Sub Method1()
End Interface

<Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"), _
  ClassInterface(ClassInterfaceType.None)> _
Public Class MyComponent
    Implements IMy

    Public Sub Method1() Implements IMy.Method1

    End Sub
End Class

In this example, you have an interface IMy with Method1() that you intend to expose to the client. For the IMy interface you have declared its InterfaceType attribute as ComInterfaceType.InterfaceIsDual. This makes the methods of the interface available for both scripting and strongly typed clients. The class MyComponent implements that interface. Furthermore, you have declared its ClassInterface attribute as ClassInterfaceType.None. The types exposed to a COM client are shown in Figure 8-17.

Effect of ClassInterfaceType.None

Figure 8-17. Effect of ClassInterfaceType.None

The use of ClassInterfaceType.None brings a .NET component back to how a real COM component should behave—it supports interfaces and does not expose anything to the client on its own. While there are three options to expose a .NET class for COM interoperability, only this last one is meaningful to use.

IN A NUTSHELL

Set the ClassInterface attribute to ClassInterfaceType.None on .NET components you want to expose for COM interoperability; do not use the default. Expose the desired methods to the COM clients through a .NET interface by setting its InterfaceType attribute to ComInterfaceType.InterfaceIsDual.

GOTCHA #73 Simply tur ning the switch for COM interop is dangerous

Checking the project setting Register for COM Interop prepares your assembly for COM interoperability. When your code is compiled, Visual Studio invokes regasm to place the codebase entry details for the assembly into the Registry at HKEY_CLASSES_ROOTCLSID...InprocServer32. When things are that easy, you probably wonder “what’s the catch?” There are several things to ponder. First, consider some of the .NET capabilities that are not quite supported in COM, or must not be used with COM:

  • COM doesn’t support method overloading.

  • COM doesn’t handle static variables well.

  • CoCreateInstance() expects a no-parameter constructor.

  • You may not intend to expose all of your public classes for COM interoperability (the COMVisible attribute affects this) .

  • It is better to define a GUID for the classes you wish to expose.

  • It is better to expose functionality through interfaces instead of classes.

Say you are working on your code one afternoon and someone walks in saying, “Hey I need to access this class through COM, can I?” You say “Sure, all I need to do is turn on the settings to register the assembly for COM interop.” This will result in the COM clients talking to your assembly in the manner shown in Figure 8-18.

Improperly exposing an assembly for COM Interop

Figure 8-18. Improperly exposing an assembly for COM Interop

Ask the questions:

  • Do you want every public class in your assembly to be visible for COM interop?

  • Do you want to control which methods of your class are available for COM clients?

  • Should you rethink what the COM client wants to do with your assembly?

  • Should you provide a more coarsely grained object for COM clients to interact with? That is, should you provide one class with the functionality the COM client actually needs, rather than just exposing a number of your .NET classes?

Most likely, you wrote your assembly with .NET clients in mind. When you are approached with a request to expose your classes for COM interoperability, it may be better to write a separate façade assembly that the COM client will interact with, as shown in Figure 8-19.

You provide a wrapper or façade that takes requests from COM clients and routes them to one or more .NET classes. The wrapper classes do not expose any COM interfaces directly to the COM client; instead they implement .NET interfaces that are available and accessible to those clients (see Gotcha #72, "All but one of the ClassInterface options are ineffective“).

Why should you do this? The wrapper or façade approach gives you a number of advantages:

  • You can control what a COM client actually sees.

  • You don’t expose all public classes in your assembly.

  • The façade is written with interoperability in mind and hence is more cohesive.

Exposing .NET components for COM interop using Wrappers

Figure 8-19. Exposing .NET components for COM interop using Wrappers

  • It is easier to deal with changes from the COM client point of view.

  • Over time, if applications that use COM are upgraded to use .NET, this is easier to manage—the wrapper simply goes away without affecting anything else.

IN A NUTSHELL

Do not open up an assembly for arbitrary COM access. Instead provide a façade for the COM clients to interact through.

GOTCHA #74 ServicedComponents implemented inconsistently on XP and 2003

Enterprise Services in .NET provide ServicedComponents, which give you COM+ features like object pooling, just-in-time activation, and transactions. I learned a few lessons when I ran into this gotcha. Enterprise Services don’t behave the same on different versions of Windows. If you are using transactions and expect your application to run on Windows 2000, XP, and 2003, there are things that you need to be aware of.

Say you want to perform an operation on some objects, and when it completes you may decide to commit or abort the transaction. You would expect this to be pretty straightforward. Consider Examples 8-15 and 8-16.

Example 8-15. Using Transactions in Enterprise Services (C#)

C# (ES)

// Factory.cs part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    [Transaction(TransactionOption.Required), JustInTimeActivation]
    public class Factory : ServicedComponent
    {
        public Comp CreateComp(int key)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            Comp theComp = new Comp();
            theComp.init(key);

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;

            return theComp;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ContextUtil.DeactivateOnReturn = true;
            }
            base.Dispose (disposing);
        }

    }
}


// Comp.cs part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    [Transaction(TransactionOption.Required), JustInTimeActivation]
    public class Comp : ServicedComponent
    {
        private int theKey;
        private int theVal;

        internal void init(int key)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            theKey = key;
            theVal = key * 10;

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;
        }

        public int GetValue()
        {
            return theVal;
        }

        public void SetValue(int val)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            theVal = val;

            if (val < 0)
            {
                ContextUtil.DeactivateOnReturn = true;
                throw new ApplicationException(
                        "Invalid value");
            }

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;
        }
    }
}


//Test.cs part of ESUser.exe
using System;
using ESLib;

namespace ESUser
{
    class Test
    {
        public static void Work()
        {
            using(Factory theFactory = new Factory())
            {

                try
                {
                    Comp component1
                        = theFactory.CreateComp(1);
                    Comp component2
                        = theFactory.CreateComp(2);

                    Console.WriteLine(component1.GetValue());
                    Console.WriteLine(component2.GetValue());

                 component1.SetValue(1);
                    component2.SetValue(-1);

                    Console.WriteLine(component1.GetValue());
                    Console.WriteLine(component2.GetValue());
                }
                catch(Exception ex)
                {
                    Console.WriteLine("Oops: " + ex.Message);
                }
            } // theFactory is Disposed here.
        }

        public static void Main()
        {
            try
            {
                Work();
            }
            catch(Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }
    }
}

Example 8-16. Using Transactions in Enterprise Services (VB.NET)

VB.NET (ES)

' Factory.vb part of ESLib.dll
Imports System.EnterpriseServices

<Transaction(TransactionOption.Required), JustInTimeActivation()> _
Public Class Factory
    Inherits ServicedComponent

    Public Function CreateComp(ByVal key As Integer) As Comp
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        Dim theComp As New Comp

        theComp.init(key)

        ContextUtil.MyTransactionVote = TransactionVote.Commit

        Return theCOmp
    End Function
    Protected Overloads Overrides Sub Dispose( _
           ByVal disposing As Boolean)
        If disposing Then
            ContextUtil.DeactivateOnReturn = True
        End If

        MyBase.Dispose(disposing)
    End Sub
End Class


' Comp.vb part of ESLib.dll
Imports System.EnterpriseServices

<Transaction(TransactionOption.Required), JustInTimeActivation()> _
Public Class Comp
    Inherits ServicedComponent

    Private theKey As Integer
    Private theVal As Integer

    Friend Sub init(ByVal key As Integer)
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        theKey = key
        theVal = key * 10

        ContextUtil.MyTransactionVote = TransactionVote.Commit
    End Sub


    Public Function GetValue() As Integer
        Return theVal
    End Function

    Public Sub SetValue(ByVal val As Integer)
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        theVal = val

        If val < 0 Then
            ContextUtil.DeactivateOnReturn = True
            Throw New ApplicationException("Invalid value")
        End If

        ContextUtil.MyTransactionVote = TransactionVote.Commit
    End Sub
End Class


'Test.vb part of ESUser.exe
Imports ESLib

Module Test
    Public Sub Work()
        Dim theFactory As New Factory

        Try
            Dim component1 As Comp = theFactory.CreateComp(1)
            Dim component2 As Comp = theFactory.CreateComp(2)

            Console.WriteLine(component1.GetValue())
            Console.WriteLine(component2.GetValue())

            component1.SetValue(1)
            component2.SetValue(-1)

            Console.WriteLine(component1.GetValue())
            Console.WriteLine(component2.GetValue())
        Catch ex As Exception
            Console.WriteLine("Oops: " + ex.Message)
        Finally
            theFactory.Dispose()
        End Try
    End Sub

    Public Sub Main()
        Try
            Work()
        Catch ex As Exception
            Console.WriteLine("Error:" + ex.Message)
        End Try
    End Sub
End Module

Comp is a ServicedComponent with one method, Method1(). This method throws an exception if the parameter val is less than 0. Note that Comp has its Transaction attribute set as TransactionOption.Required . Factory is a ServicedComponent that is used to create objects of Comp. In the Main() method of Test, you create two instances of Comp by calling the CreateComp() method of the Factory. You then invoke Method1() on the two instances. The exception that is thrown by the second call to Method1() is displayed in the catch handler block.

When you execute the code on Windows Server 2003, it runs as you would expect and produces the result shown in Figure 8-20.

Output from Example 8-15 on Windows 2003 Server

Figure 8-20. Output from Example 8-15 on Windows 2003 Server

But if you run the same code on Windows XP, you get the error shown in Figure 8-21.

Output from Example 8-15 on Windows XP

Figure 8-21. Output from Example 8-15 on Windows XP

The reason for this problem is that while one of the Comp components has voted to abort the transaction, the root object (the instance of the Factory) that created this object wants to commit it. Windows Server 2003 has no problem with that. But Windows XP does.

The solution is for the component Comp to tell the Factory that it is setting the transaction vote to Abort. The root object must then set its vote to Abort as well. This is not ideal because now the component needs to have a back pointer to the root object that created it. The code that does this is shown in Example 8-17 and Example 8-18. It uses an interface to break the cyclic dependency between the component and its factory.

Example 8-17. Communicating with the root object (C#)

C# (ES)

// Factory.cs part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    public interface ITransactionCoordinator
    {
        void SetVoteToAbort();
    }

    [Transaction(TransactionOption.Required), JustInTimeActivation]
    public class Factory : ServicedComponent, ITransactionCoordinator
    {
        public Comp CreateComp(int key)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            Comp theComp = new Comp();

            theComp.TheTransactionCoordinator = this;

            theComp.init(key);

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;

            return theComp;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ContextUtil.DeactivateOnReturn = true;
            }
            base.Dispose (disposing);
        }

        #region ITransactionCoordinator Members

        public void SetVoteToAbort()
        {
            ContextUtil.MyTransactionVote = TransactionVote.Abort;
        }

        #endregion
    }
}


// Comp.cs part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    [Transaction(TransactionOption.Required), JustInTimeActivation]
    public class Comp : ServicedComponent
    {
        private int theKey;
        private int theVal;

        private ITransactionCoordinator theTXNCoordinator;

        internal ITransactionCoordinator TheTransactionCoordinator
        {
            get { return theTXNCoordinator; }
            set { theTXNCoordinator = value; }
        }

        internal void init(int key)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            theKey = key;
            theVal = key * 10;

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;
        }

        public int GetValue()
        {
            return theVal;
        }

        public void SetValue(int val)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            theVal = val;

            if (val < 0)
            {
                ContextUtil.DeactivateOnReturn = true;
                if (theTXNCoordinator != null)
                    theTXNCoordinator.SetVoteToAbort();

                throw new ApplicationException(
                    "Invalid value");
            }

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;
        }
    }
}

Example 8-18. Communicating with the root object (VB.NET)

VB.NET (ES)

' Factory.vb part of ESLib.dll
Imports System.EnterpriseServices

Public Interface ITransactionCoordinator
    Sub SetVoteToAbort()
End Interface

<Transaction(TransactionOption.Required), JustInTimeActivation()> _
Public Class Factory
    Inherits ServicedComponent
    Implements ITransactionCoordinator

    Public Function CreateComp(ByVal key As Integer) As Comp
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        Dim theComp As New Comp

        theComp.TheTransactionCoordinator = Me

        theCOmp.init(key)

        ContextUtil.MyTransactionVote = TransactionVote.Commit

        Return theCOmp
    End Function
    Protected Overloads Overrides Sub Dispose( _
        ByVal disposing As Boolean)
        If disposing Then
            ContextUtil.DeactivateOnReturn = True
        End If

        MyBase.Dispose(disposing)
    End Sub

    Public Sub SetVoteToAbort() _
        Implements ITransactionCoordinator.SetVoteToAbort
        ContextUtil.MyTransactionVote() = TransactionVote.Abort
    End Sub
End Class


' Comp.vb part of ESLib.dll
Imports System.EnterpriseServices

<Transaction(TransactionOption.Required), JustInTimeActivation()> _
Public Class Comp
    Inherits ServicedComponent

    Private theKey As Integer
    Private theVal As Integer

    Private theTXNCoordinator As ITransactionCoordinator

    Friend Property TheTransactionCoordinator() _
        As ITransactionCoordinator
        Get
            Return theTXNCoordinator
        End Get
        Set(ByVal Value As ITransactionCoordinator)
            theTXNCoordinator = Value
        End Set
    End Property

    Friend Sub init(ByVal key As Integer)
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        theKey = key
        theVal = key * 10

        ContextUtil.MyTransactionVote = TransactionVote.Commit
    End Sub


    Public Function GetValue() As Integer
        Return theVal
    End Function

    Public Sub SetValue(ByVal val As Integer)
        ContextUtil.MyTransactionVote = TransactionVote.Abort

        theVal = val

        If val < 0 Then
            ContextUtil.DeactivateOnReturn = True

            If Not theTXNCoordinator Is Nothing Then
                theTXNCoordinator.SetVoteToAbort()
            End If

            Throw New ApplicationException("Invalid value")
        End If

        ContextUtil.MyTransactionVote = TransactionVote.Commit
    End Sub
End Class

Note that Method1() informs the root object that it wants to abort the transaction. The root object then sets its transaction vote to Abort. After this modification, the program produces the same result on XP as on Windows Server 2003.

One moral from this story is to run unit tests not just on your machine but also on every supported platform. How practical is this? Well, it’s not just practical, it’s highly feasible with project automation and continuous integration tools like NAnt, NUnit and Cruise Control .NET.

Tip

If you have hardware/cost limitations, you can use a product such as VirtualPC or VMWare to run these tests on different platforms on the same hardware.

IN A NUTSHELL

ServicedComponents don’t behave the same on all Windows platforms. Make sure you test early and often on all supported versions.

GOTCHA #75 AutoComplete comes with undesirable side effects

The AutoComplete attribute in Enterprise Services provides a very convenient way to communicate the intent to commit or abort a transaction. When a method marked with that attribute is invoked, the method automatically votes to commit the transaction if the method is successful. If the method fails, which is indicated by throwing an exception, the vote is automatically set to abort. This sounds pretty logical, so what’s the concern? Let’s examine the code in Example 8-19.

Example 8-19. Effect of AutoComplete

C# (ESAutoComplete)

//MyComp.cs as part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    [Transaction(TransactionOption.Required)]
    public class MyComp : ServicedComponent
    {
        private string theMessage = "UnSet";

        [AutoComplete]
        public void SetInfo(string msg)
        {
            theMessage = msg;
        }

        [AutoComplete]
        public string GetInfo()
        {
            return theMessage;
        }
    }
}

//Test.cs as part of ESUser.exe
using System;
using ESLib;

namespace ESUser
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyComp theComp = new MyComp();

            theComp.SetInfo("hello");

            Console.WriteLine(theComp.GetInfo());
        }
    }
}

VB.NET (ESAutoComplete)

'MyComp.vb as part of ESLib.dll
Imports System.EnterpriseServices

<Transaction(TransactionOption.Required)> _
Public Class MyComp
    Inherits ServicedComponent

    Private theMessage As String = "UnSet"

    <AutoComplete()> _
    Public Sub SetInfo(ByVal msg As String)
        theMessage = msg
    End Sub

    <AutoComplete()> _
    Public Function GetInfo() As String
        Return theMessage
    End Function

End Class


'Test.vb as part of ESUser.exe

Imports ESLib

Module Test

    Sub Main()
        Dim theComp As New MyComp

        theComp.SetInfo("hello")

        Console.WriteLine(theComp.GetInfo())
    End Sub

End Module

In this example, you create an instance of MyComp in the Main() method of the Test class. (The MyComp class stores a field named theMessage and initializes it to “UnSet.”) You then call SetInfo() to set theMessage to “hello,” then call GetInfo() to read it back. When the above code is executed you get the output shown in Figure 8-22.

Output from Example 8-19

Figure 8-22. Output from Example 8-19

What went wrong? Why didn’t you get the expected result of “hello?”

The client is not dealing directly with the MyComp object, but with an invisible proxy. When the SetInfo() method is called, the method executes on the actual component. At the end of the method, due to the AutoComplete attribute, the transaction is committed by an internal call to SetComplete().

However, this call has a side effect. It not only sets the vote, it also sets the ContextUtil.DeactivateOnReturn property to true. As a result, the object is deactivated upon the return from the SetInfo() method. In other words, as soon as the method completes, the object is destroyed (kind of like working for the Mafia). When the GetInfo() method is invoked again using the proxy reference, another instance of the object is created automatically. theMessage is initialized to “UnSet” in this newer object, and that’s what GetInfo() returns to you.

Let’s consider the code changes in Example 8-20.

Example 8-20. Safely communicating intent to commit

C# (ESAutoComplete)

//MyComp.cs as part of ESLib.dll
using System;
using System.EnterpriseServices;

namespace ESLib
{
    [Transaction(TransactionOption.Required)]
    public class MyComp : ServicedComponent
    {
        private string theMessage = "UnSet";

        //[AutoComplete]
        public void SetInfo(string msg)
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            theMessage = msg;

            // If something is wrong, throw exception
            // and the vote will remain in Abort.

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;
        }

        //[AutoComplete]
        public string GetInfo()
        {
            ContextUtil.MyTransactionVote
                = TransactionVote.Abort;

            // If something is wrong, throw exception
            // and the vote will remain in Abort.

            ContextUtil.MyTransactionVote
                = TransactionVote.Commit;

            return theMessage;
        }
    }
}

VB.NET (ESAutoComplete)

'MyComp.vb as part of ESLib.dll
Imports System.EnterpriseServices

<Transaction(TransactionOption.Required)> _
Public Class MyComp
    Inherits ServicedComponent

    Private theMessage As String = "UnSet"

    '<AutoComplete()> _
    Public Sub SetInfo(ByVal msg As String)
        ContextUtil.MyTransactionVote _
            = TransactionVote.Abort

        theMessage = msg

        ' If something is wrong, throw exception
        ' and the vote will remain in Abort.

        ContextUtil.MyTransactionVote _
            = TransactionVote.Commit
    End Sub

    '<AutoComplete()> _
    Public Function GetInfo() As String
        ContextUtil.MyTransactionVote _
            = TransactionVote.Abort

        ' If something is wrong, throw exception
        ' and the vote will remain in Abort.

        ContextUtil.MyTransactionVote _
            = TransactionVote.Commit

        Return theMessage
    End Function

End Class

In the methods of the component, you first set the transaction vote to Abort. If the method is successful, you change the vote to Commit. If the method is not successful, you throw an exception, leaving the transaction vote as Abort. You do not use the AutoComplete attribute on the methods. The advantage of this is that the object is not automatically deactivated without your explicit intent. The output after the above code change is shown in Figure 8-23.

Output after code change in Example 8-20

Figure 8-23. Output after code change in Example 8-20

The AutoComplete attribute comes with a side effect. Understand its impact on an object’s lifetime. It is better to explicitly set the transaction vote to Abort or Commit, rather than using the AutoComplete attribute. Use AutoComplete only if you really want the object to be discarded when the method completes.

IN A NUTSHELL

Instead of using the AutoComplete attribute, directly set the transaction vote in your code. Avoid the side effect of AutoComplete that automatically deactivates the component.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.126.180