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.
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
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.
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.
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.
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" and Gotcha #70, "Spattering access to COM components makes code hard to maintain.”
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
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(); } } }
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.
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.”
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 #65, "Release of COM object is confusing" and Gotcha #70, "Spattering access to COM components makes code hard to maintain.”
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.
Example 8-3. Effect of apartment-related attributes
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.
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.
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
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.
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,” Gotcha #69, "STAThread attribute may have no effect on your methods,” and Gotcha #70, "Spattering access to COM components makes code hard to maintain.”
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
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(); } } }
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.
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.
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 #67, "Cross-apartment calls are expensive,” Gotcha #69, "STAThread attribute may have no effect on your methods,” and Gotcha #70, "Spattering access to COM components makes code hard to maintain.”
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
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(); } } }
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.
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
//... 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(); } } }
'... 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.
For threads that interact with COM components, set the apartment of the thread yourself; don’t use method attributes to do so.
Gotcha #67, "Cross-apartment calls are expensive,” Gotcha #68, "Default apartment of main thread is inconsistent across languages,” and 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.
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.
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.
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 #65, "Release of COM object is confusing,” Gotcha #66, "Using interface pointers after calling ReleaseComObject() will fail,” Gotcha #67, "Cross-apartment calls are expensive,” Gotcha #68, "Default apartment of main thread is inconsistent across languages,” and Gotcha #69, "STAThread attribute may have no effect on your methods.”
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
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.
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
using System;
using System.Runtime.InteropServices;
namespace ALib
{
[Guid("53DEF193-D7A4-4ce3-938E-A7A35B5F7AB7"),
ClassInterface(ClassInterfaceType.AutoDispatch)]
public class MyComponent
{
public void Method1()
{
//...
}
}
}
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).
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" and Gotcha #73, "Simply tur ning the switch for COM interop is dangerous.”
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.
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
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.
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() { //... } } }
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.
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.
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 #71, "Auto-generating GUID for your classes leads to versioning woes" and 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:
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.
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.
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.
Do not open up an assembly for arbitrary COM access. Instead provide a façade for the COM clients to interact through.
Gotcha #71, "Auto-generating GUID for your classes leads to versioning woes" and Gotcha #72, "All but one of the ClassInterface options are ineffective.”
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#)
// 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)
' 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.
But if you run the same code on Windows XP, you get the error shown in Figure 8-21.
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#)
// 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)
' 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.
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.
ServicedComponent
s 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
//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()); } } }
'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.
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
//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; } } }
'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.
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.
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.
Gotcha #74, "ServicedComponents implemented inconsistently on XP and 2003.”
3.147.126.180