Monday, January 12, 2009

Assembly Resolution - How AppDomains Change from Desktop to Web

So, for our build wizard, I needed to load some DLLs into a child app domain that could orchestrate all of our build script work, then unload the domain. Freeing up the .NET Assemblies in this way lets us always run off the latest/greatest of the DLL without the need to reset IIS.

I put some significant effort into getting the child AppDomain code to work from a mock command line app before plunging into the web world. This was my first exposure to a non-academic use of AppDomains, so I wanted to have a playground to get comfortable and prototype some code. It didn't take long to get something that was working. So, I imported the classes from my command line app into my web app I had been working on.

In the command line app, I had been initializing the AppDomain like so:

//setup a child appdomain
string appDomainUniqueNamePerUser = string.Format("WF Child Domain", Guid.NewGuid().ToString("D"));
AppDomain childAD = AppDomain.CreateDomain(appDomainUniqueNamePerUser);
assemblyResolvePath = appDomainBaseDir + @"\BuildScripts";
childAD.AssemblyResolve += new ResolveEventHandler(childAD_AssemblyResolve);

//get my proxy class for crossing the appdomain boundary
WFWP remoteWorker =
(WFWP)childAD.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "FE.BuildControl.WFWP");

Suddently, I was getting strange error "Type is not resolved for member 'FE.BuildControl.WFWP'". What?! The exact same code just worked from the command line.

After much painful troubleshooting, I discovered this error is more of a red herring than anything. It points to assembly loading problems (imagine that!). So, I discovered through the Fusion log and FileMon that my assembly resolution for the WFWP class was interrogating my C:\windows\Microsoft.NET\Framework\v2.0.... folder. "Why there?", you ask....

Well, it turns out that AppDomains by default will assume the same base directory as the .exe they are running from. Yes...the .EXE they are running from. :) So, when I was running as a command line app, it was probing for assemblies in the same folder I was executing from. Naturally, my referenced DLLs existed there and all was well. When in a web app, your .EXE is aspnet_wp.exe (Windows XP) or W3WP.exe (Win 2003). So, those reside in the .NET 2.0 framework folder, mentioned above. So, it was doing exactly what I told it to do, but that's not what I meant.

Enter the AppDomainSetup class:

string appDomainUniqueNamePerUser = string.Format("WF Child Domain", Guid.NewGuid().ToString("D"));
AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = appDomainBaseDir;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.PrivateBinPath = "Bin";
AppDomain childAD = AppDomain.CreateDomain(appDomainUniqueNamePerUser, null, ads);

By specifying my physical folder that I'm using as a virtual directory in IIS as my "ApplicationBase" above, I can cause the assembly resolution logic in .NET to look for my DLLs there instead. The "PrivateBinPath" just helps it along and says, "DLLs in this application will be in this folder". Now, my child appdomain executes like a champ!

Moral of the story: .NET simplifies/standardizes a lot of things compared to prior languages, but one cannot assume that a class will behave identically when put in a different context (such as another hosted process).

That's all for now. Take care,