NUnit and IThreadLogicalAffinative - AppDomains, CallContexts and weird, weird problems
Recently, at one of my clients we've been spending some quality time with NUnit. However, we ran into one major problem that you too may have had. This is actually more of a discussion on Remoting, but it's relevent to those using NUnit.
To do it's work, NUnit creates a separate AppDomain into which it loads the assemblies that will be tested. This isolates the code to be tested from the NUnit code that runs the test, and that's a smart move. It also means that the same NUnit process can unload the assemblies and replace them with newer versions without having to recycle the entire NUnit process.
In our scenario, we have an application framework. Let's call it MyFx.dll. This contains essential runtime components for powering the application. In our case, it's an n-tier app that uses Remoting. An assembly called MyModule.dll contains some implementation code and it's dependent on MyFx.dll.
Here's some code:
Console.WriteLine("Booting runtime environment...");
MyService.Start(); // MyService is implemented in MyFx.dll
// ...some work here...
Console.WriteLine("About to run some activity...");
Foo.Bar(); // Foo is implemented in MyModule.dll.
Outside of NUnit, this code works fine. However, running inside of NUnit, the line Console.WriteLine("About to run some activity..."); was failing! How could a Console.WriteLine() fail?
The exception reported that the assembly MyFx.dll could not be found. This is particuarly odd as a call MyService.Start() worked OK. (Remember, MyService is implemented in MyFx.dll.) Therefore, MyFx.dll has been loaded. Why were we getting an assembly load exception when the assembly was loaded?
Digging around in the exception message, what was happening is that calls to Console.xxx and Trace.xxx were being grabbed by a TraceListener running inside of the NUnit primary AppDomain. This call necessitated a call across an appdomain boundary, hence necessitated a Remoting call.
What made sense now is that the load request MyFx.dll was failing because it was the NUnit appdomain was demanding it. As MyFx.dll was not installed into either the NUnit private path or the GAC, rightly the request was failing and our test was failing on the Console.WriteLine() call.
I was baffled, but my colleague Tony wrapped this small piece of code in so many tests he eventually chased the problem down to a class we have called ClientContext that implemented ILogicalThreadAffinative. An instance of this was being created in MyService.Start() and added to the System.Runtime.Remoting.Messaging.CallContext bucket.
CallContext is a really useful Remoting class that enables you to piggyback date on every single call across the boundary. In ClientContext, we store a token which identifies the user. We do this so that requests into the application server can determine who the user is and determine his or her rights.
Under the hood, when a call is made that has to go over a boundary, Remoting will examine this CallContext and anything that implements ILogicalThreadAffinative is serialized and passed over as part of the message. When the end point receives the message, these objects are deserialized and put into the CallContext bucket at the remote end. In our case ClientContext was implemented in MyFx.dll. When Remotiing tried to unpack the message, it attempted to deserialze an object of this type and - quite rightly - failed because the assembly was not available in the private path of the primary NUnit appdomain.
The (rather obvious) lesson here is that if you put something in the CallContext, it has to be available at both ends of the application. In our production scenario, this is true because we install MyFx.dll on the server and on the client, hence everything works fine and we'd never seen the problem!
However, with NUnit this causes us some grief. One solution here is to take the CallContext class out and put it in its own assembly and either install this assembly in the GAC or to put it in the private path of the NUnit folder. I personally think this is a better solution than putting the entire MyFx.dll into the GAC or the NUnit folder.