Mark Gilbert's Blog

Science and technology, served light and fluffy.

This one, not THAT one! AppDomains, Dynamic Assembly Loading, and the AssemblyResolve event.

One of my two long-term “home” projects (the ones that I do 15-minutes at a time) is called Assistant and requires a “plug-in” architecture, meaning you write to an interface, drop the compiled “module” DLL into a special folder on the file system, and the app can dynamically load it.  Yeah, and it was about as easy as it sounds to get it to work correctly.

I had done dynamic loading of DLLs in the past, but always in the same AppDomain as the current executable.  That, apparently, is the easy way to do it.  I wanted to take Assistant’s architecture one step further though – each new module should be loaded into its own AppDomain.  This keeps the 3rd-party module in its own sandbox, so if it crashes it doesn’t take the main application down with it.  More to the point for my immediate needs – unit testing – I needed to be able to unload the DLL so I could reset the state of the app for the next test.  Long-story short, there is no way to unload a single DLL, but you can unload an entire AppDomain and every DLL in it – hence, AppDomains for all.

Ok, so before I even got to unloading AppDomains, I needed to be able to 1) create a new one and 2) load a DLL into it.  You’d think the hard stuff wouldn’t hit until at LEAST step 3, but then, you’d be wrong.

Step 1, creating the new AppDomain does in fact turn out to be simple:

Private Sub CreateNewAppDomain()
   Dim MyAppDomainName As String
   Dim MyAppDomainSetup As AppDomainSetup

   MyAppDomainName = Guid.NewGuid.ToString
   MyAppDomainSetup = New AppDomainSetup
   MyAppDomainSetup.ApplicationName = MyAppDomainName
   MyAppDomainSetup.ApplicationBase = Me._ModuleFolder.FullName
   Me._ModulesAppDomain = AppDomain.CreateDomain(MyAppDomainName, Nothing, MyAppDomainSetup)
End Sub

 

The ApplicationBase property is set to the folder where the new module (3rd-party DLL) is stored.  The new app domain is stored in a private class variable called _ModulesAppDomain because the class that this code comes from is responsible for loading up only one DLL ever.  I used this post from Rick Strahl’s blog as the basis for my code above: http://www.west-wind.com/WebLog/posts/601200.aspx.

Step 2, then, is to load the DLL into the newly-created AppDomain.  This step was the “deceptively easy” part (“deceptively easy” is a phrase which means “will not work until there are at least a dozen forehead-shaped dents in the closest wall”):

Me._RawAssembly = System.IO.File.ReadAllBytes(CurrentFile.FullName)
CurrentAssembly = Me._ModulesAppDomain.Load(Me._RawAssembly)

 

The _RawAssembly private variable is simply a Byte array, and CurrentAssembly is declared as System.Reflection.Assembly.  Basically what I’m doing is trying to avoid locking the file on the file system by loading it up into an in-memory Byte array, and then dropping it into the new AppDomain from there.  This allowed me to run a test that copies a DLL into the right folder, invoke the class that contains both of the above snippets to load that DLL, check that it got loaded correctly, destroy the app domain, and finally delete the file I copied in the first place.

What I was finding was that despite reading the file in as a Byte array, and then loading that Byte array into the AppDomain, it still couldn’t find the file.  After setting up the Fusion Log Viewer to see what was happening, I discovered it the fusion engine was looking for my DLL in several folders tied to NUnit (since that was the application everything was being run under).  At no point was it looking in the folder I gave it.

After a little more tinkering, I discovered that I could handle the AssemblyResolve event to give the fusion engine a custom path to look in for the DLL.  So, CreateNewAppDomain() gets modified to the following:

Private Sub CreateNewAppDomain()
   Dim MyAppDomainName As String
   Dim MyAppDomainSetup As AppDomainSetup
   MyAppDomainName = Guid.NewGuid.ToString
   MyAppDomainSetup = New AppDomainSetup
   MyAppDomainSetup.ApplicationName = MyAppDomainName
   MyAppDomainSetup.ApplicationBase = Me._ModuleFolder.FullName
   Me._ModulesAppDomain = AppDomain.CreateDomain(MyAppDomainName, Nothing, MyAppDomainSetup)
   AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf Me.CurrentDomain_AssemblyResolve
End Sub
And then I created the event handler itself:
Private Function CurrentDomain_AssemblyResolve(ByVal sender As Object, ByVal args As ResolveEventArgs) As Assembly
    Dim Parts() As String = args.Name.Split(",")
    Dim File As String = Me._ModuleFolder.FullName & "\" & Parts(0).Trim & ".dll"

    Me._RawAssembly = System.IO.File.ReadAllBytes(File)
    Return Assembly.Load(Me._RawAssembly)
End Function

 

Again, I based this method on Strahl’s article (see above). This allowed the fusion engine to find and load the file.

Unloading the AppDomain/DLL now requires a single line of code:

AppDomain.Unload(Me._ModulesAppDomain)

 

Phew.  I went on my merry way, writing tests that would load up DLLs, run tests against my class (to make sure the properties were getting set correctly), and unload the DLL in preparation for the next test.  I thought I had finally built a reliable foundation for my plug-ins.

Until I wrote a test that loaded two assemblies in two different folders.

The first assembly loaded just fine, but the second one failed.  After digging into it with the Fusion Log Viewer again, I found that the fusion engine was trying to look for the second assembly in the first assembly’s folder.  I decided to step through the code to find out exactly where it I had a wire crossed.  What I found was that the AssemblyResolve handler from the first module was firing for both the first and second DLLs.

My first reaction was that this was a threading issue.  All of the above code runs contained in a class, and I’m instantiating multiple objects of that class – one for each DLL I’m trying to load.  As a result, I expect multiple copies of my AssemblyResolve handler to be in memory.  The fusion engine is raising the AssemblyResolve event, and it’s being handled by the first copy of the handler it finds.  As it turns out, however, creating a new app domain does NOT automatically create a new thread for it to run on.

At this point I’m back to the question “how can I ensure that the correct copy of my event handler is used?”

The answer was, I cheated – I made sure there was only ONE handler in memory when the time came to use it.  The AssemblyResolve handler is only needed to find the DLL initially.  It won’t be used once I have it loaded into memory as an Assembly object.  So, why not drop the handler immediately after using it?  That would ensure there would be, at most, one copy of the handler in memory at any given time.  More to the point – the one CORRECT copy will be in memory.

So, first I call CreateNewAppDomain which adds the handler.  Then I load the DLL into the AppDomain.  Finally, I execute this line:

RemoveHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf Me.CurrentDomain_AssemblyResolve

 

This allowed my unit tests for loading multiple modules to work as expected, and I could finally move on to Step 3.

Yay, Step 3.  Where the hard stuff was supposed to reside.

Advertisements

March 25, 2010 - Posted by | Visual Studio/.NET

6 Comments

  1. Just a note, I ran into something similar, but realized that and Assembly.Load* call will always return an Assembly object. If you want the handler to raise multiple events, you have to return null/none if you cannot actually load the assembly.

    Your “eturn Assembly.Load(Me._RawAssembly)” will cause this event to always be the only one invoked.

    Comment by Nick | April 6, 2010

  2. Hi Mark,
    I have a similar scenario to implement and I was loading my assemblies using byte[] and it solved the problem of dll locking. I am able to replace my third party dll’s. However using byte[], there is a leak in memory and my application is just consuming more memory (typically 1 MB for every dll loading at runtime). Can you suggest a solution to load assemblies at runtime without memory leak?

    Thanks
    PV

    Comment by PV | February 1, 2011

    • @PV: I would start by narrowing down when the leaking starts. If you were to load and then immediately unload the assembly, do you see the leak appear? Or does the leak only manifest when you use something in the assembly? If the problem is the former, make sure you are releasing everything tied to the assembly that you’re loading up so that the garbage collector can do its job. For instance, the above line where I show how to unload the app domain is actually a little simplified; the production logic is this:

      If (Not IsNothing(Me._ModulesAppDomain)) Then AppDomain.Unload(Me._ModulesAppDomain)

      Me._ModulesAppDomain = Nothing

      I make sure to set the private variable _ModulesAppDomain to Nothing so it’s not being kept around indefinitely.

      If your testing shows that loading and unloading assemblies doesn’t produce the leak, but instantiating objects in the assembly does, even after unload, I’d look at what those classes were doing. Are they loading unmanaged items into memory, and then not cleaning up after themselves when they’re destroyed?

      Good luck!

      Comment by markegilbert | February 2, 2011

  3. Thanks for your inputs. There is memory leak when I load the assembly irrespective of usage of types in it. Assembly objects are disposed properly. I also tried using garbage collector, it did not help me.After going through couple of articles and my testing, I understood that the memory leak is due to loading assembly using byte[]. The only solution which I see is using AppDomain. However I want to restrict myself from using AppDomain because my app. is a multi-threaded app using ThreadPools, so i’m not sure on the new concerns that might rise. My interest is to load new assemblies at runtime without locking the assembly file. Please suggest

    Comment by PV | February 2, 2011

    • @PV: If you aren’t using a new AppDomain to load the new assembly, then it is loading into the main application’s AppDomain, correct? That means at least some parts of it will still hang around until your main application closes. The operating system can’t reclaim those resources until everything in the appdomain shuts down.

      There may be unintended side effects of using AppDomains, certainly. However, the AppDomain appears to be the recommended approach for managing the various portions of your application (not only for allocation and deallocation, but also for application stability and security). I’d recommend going this route.

      Comment by markegilbert | February 3, 2011

  4. Thanks for your suggestion. Will let you know if I discover anything interesting.

    Comment by PV | February 3, 2011


Sorry, the comment form is closed at this time.

%d bloggers like this: