S h o r t S t o r i e s

// Tales from software development

Archive for August 2009

Toxic mix: .NET Remoting, COM Interop, and RegAsm /codebase

leave a comment »

The project I’ve been working on for the past few months includes a .NET assembly that exposes a COM interface to a legacy desktop application. It needs to call up to eight data servers to service each client request and so uses multithreading to do this simultaneously with a minimal response time.

The COM Interop assembly could be deployed to the GAC but as the assembly isn’t shared it seemed reasonable to install it in a folder under the client application and use the /codebase switch when registering it with the RegAsm.exe tool. The /codebase switch indicates to .NET that it should use the assembly at this file system location when creating an instance for COM callers.

I thought that designing and implementing code that mixed COM Interop and multithreaded code was fairly challenging but then the customer increased the pain level by insisting that the calls to the data servers were made from a single gateway server rather than the PCs where the legacy desktop application is running. This would require some type of remoting – either .NET Remoting or Web Services.

I chose the .NET Remoting solution on the basis that it should be possible to refactor the code in the COM Interop assembly into a remotable class and therefore would require less effort than implementing a completely new interface using a web service. In fact it only took half a day and everything seemed to work perfectly when I used a .NET test harness to call the interop assembly. Of course, this wasn’t testing the COM Interop aspect of the assembly so I installed the new assembly on a PC that had the legacy desktop client installed and tried it out. It failed:

SOAP exception: Parse Error, no assembly associated with Xml key a1:http://schemas.microsoft.com/clr/nsassem/Vitality.Integration.DataInterface/DataCollection, Version=2.0
.0.0, Culture=neutral, PublicKeyToken=f529d7a12981f1ee DataItem

 
The assembly specified in the exception message was definitely loaded as it was referenced by the COM Interop assembly and instances of the classes it exposes were being created before the point of failure.

It took several hours of googling, a thorough re-reading of all of Suzanne Cook’s blogs on the Fusion loader and load contexts, and some debugging to get to the bottom of the problem. It turned out it was the /codebase switch.

Once I thought I had an understanding of the issue I enabled Fusion logging and reran the desktop application to the point of failure. The Fusion logs showed that the COM Interop assembly and the Vitality.Integration.DataInterface assembly mentioned in the exception message were being loaded in the LoadFrom context instead of the default load context:

*** Assembly Binder Log Entry  (10/08/2009 @ 12:03:47) ***
The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.
Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Running under executable  C:\Program Files\Vitality\Vitality.exe
--- A detailed error log follows.
=== Pre-bind state information ===
LOG: User = NITRON\SeaMonkey
LOG: Where-ref bind. Location = C:/Program Files/DataInterface/Vitality.Integration.DataInterface.DLL
LOG: Appbase = file:///C:/Program Files/Vitality/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = Vitality.exe
Calling assembly : (Unknown).
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: No application configuration file found.
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: Attempting download of new URL file:///C:/Program Files/DataInterface/Vitality.Integration.DataInterface.DLL.
LOG: Assembly download was successful. Attempting setup of file: C:/Program Files/DataInterface/Vitality.Integration.DataInterface.dll
LOG: Entering run-from-source setup phase.
LOG: Assembly Name is: Vitality.Integration.DataInterface, Version=2.0.0.0, Culture=neutral, PublicKeyToken=f529d7a12981f1ee
LOG: Re-apply policy for where-ref bind.
LOG: Post-policy reference: Vitality.Integration.DataInterface, Version=2.0.0.0, Culture=neutral, PublicKeyToken=f529d7a12981f1ee
LOG: GAC Lookup was unsuccessful.
LOG: Where-ref bind Codebase does not match what is found in default context. Keep the result in LoadFrom context.
LOG: Binding succeeds. Returns assembly from C:\Program Files\DataInterface\Vitality.Integration.DataInterface.dll.
LOG: Assembly is loaded in LoadFrom load context.

 
Why was the assembly loaded into the LoadFrom context ? Well, because these assemblies would only be loaded in the default load context if the .NET CLR successfully probed for them, i.e. if they were located in the same folder as the main executable or in the GAC. The only reason that the CLR had managed to load them at all was because of the /codebase switch which was telling the CLR where to get the assemblies from.

The LoadFrom context didn’t stop the COM Interop assembly successfully calling the remoted class but when the CLR attempted to deserialize the data returned it expected to find an assembly that was capable of doing this in the default load context. Although the Vitality.Integration.DataInterface was loaded, it wasn’t in the expected context. So, as far as the CLR was concerned, it could not locate an assembly capable of deserializing the return data from the remoted class. The Fusion log showed this:

*** Assembly Binder Log Entry  (10/08/2009 @ 12:03:49) ***
The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.
Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Running under executable  C:\Program Files\Vitality\Vitality.exe
--- A detailed error log follows.
=== Pre-bind state information ===
LOG: User = NITRON\SeaMonkey 
LOG: DisplayName = Vitality.Integration.DataInterface, Version=2.0.0.0, Culture=neutral, PublicKeyToken=f529d7a12981f1ee
 (Fully-specified)
LOG: Appbase = file:///C:/Program Files/Vitality/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = Vitality.exe
Calling assembly : System.Runtime.Serialization.Formatters.Soap, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
===
LOG: This bind starts in default load context.
LOG: No application configuration file found.
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: The same bind was seen before, and was failed with hr = 0x80070002.
ERR: Unrecoverable error occurred during pre-download check (hr = 0x80070002).

 
The solution is, thankfully, very simple. What’s the easiest way of ensuring that the assembly gets loaded in the default load context rather than the LoadFrom context ? By getting the CLR to load the assembly when it probes for referenced assemblies. So, either put the assembly in the GAC or place the assembly in the same folder as the main executable and don’t register it using the /codebase switch.

Interestingly enough, there is another symptom of this scenario that I also hit but didn’t appreciate what the problem was at the time. It’s not possible to cast from a type to itself if this involes two instances of the  assembly where the type is implemented being loaded in different load contexts. At one stage in the refactoring of the code in the COM Interop assembly to the remotable class, I attempted to cast a type returned by the remote instance to itself and this caused an InvalidCastException. This should have set alarm bells ringing but at the time it seemed so ridiculous that I couldn’t cast a type to itself that I just worked around the problem rather than taking it seriously. Next time I’ll know better…

Advertisements

Written by Sea Monkey

August 11, 2009 at 6:00 pm

Posted in Development

Tagged with