views:

539

answers:

5

At runtime, I'd like to be able to unload a DLL and reload a modified version of it. My first experiment went down in flames. Can anyone tell me why? Thanks in advance!

private static void Main()
{
   const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";

   // Starting out with a  version of MyLibrary.dll which only has 1 method, named Foo()
   AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
   AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
   appDomain.Load(assemblyName);
   appDomain.DomainUnload += appDomain_DomainUnload;
   AppDomain.Unload(appDomain);

   // Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
   AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
   AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
   Assembly asm2 = appDomain2.Load(assemblyName2);

   foreach (Type type in asm2.GetExportedTypes())
   {
      foreach (MemberInfo memberInfo in type.GetMembers())
      {
         string name = memberInfo.Name;
         // Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
      }
   }
}

private static void appDomain_DomainUnload(object sender, EventArgs e)
{
   // This gets called before the first breakpoint
}
+1  A: 

I have found that if you ever directly access types in an assembly it gets loaded into your own domain. So what I have had to do is create a third assembly that implements interfaces common to both assemblies. That assembly gets loaded into both domains. Then just be careful to only use interfaces from that third assembly when interacting with the external assembly. That should allow you to unload the second assembly by unloading its domain.

BlueMonkMN
Another way to avoid pulling the types into the main domain is to use AppDomain.DoCallBack. This allows you to execute code in the loaded domain. In the OP's example the foreach ... could be passed to the loaded AppDomain.
Mike Two
A: 

Okay, this is obviously my first time posting. Thanks Daniel for formatting my code (I now see the toolbar button to do that, and the preview pane, too!). I don't see a way to post a "comment" in reply to either those who asked for clarification to the original post or to the 1 answer, so I'll just post another "answer" to keep the conversation going. (Pointers appreciated on how comments are done would be appreciated).

Comments to the posting: Mitch - Went down in flames 'cause my foreach loop should have been iterating over the types in the modified DLL, not the previously loaded/unloaded one. Matthew - That might work, but I really need the case of the same file name to work. Mike Two - Not strongly named.

Comments to the answer: Blue & Mike Two - I'll noodle on your suggestions, but first I need to understand a critical aspect. I had read that you have to take care not to pull the assembly into the main app domain, and the code previously had a copy of the foreach loop before the unload. So, suspecting that accessing MethodInfos was sucking the assembly into the main app domain, I removed the loop. That's when I knew I needed to ask for help, 'cause the first DLL still wouldn't unload!

So my question is: What in the following code segment causes the main app domain to ever directly (or indirectly) access anything in the dll...why would it cause the assembly to also load in the main app domain:

appDomain.Load(assemblyName);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);

Didn't give me much confidence I'd ever actually be able to use the DLL before unloading it.

Jim C
Okay, I now see how to add a comment to my "answer", but I don't see the 'add comment'link for the posting or the real answer. Is it possible to add a comment to the post after an answer has been added, or to the first answer after a 2nd one has been added?
Jim C
Is it possible that linking an event from the other domain to a handler in your own domain caused the assembly to get loaded into your own domain? I doubt it, but is there a way to rule that possibility out and still determine if the domain is being unloaded?
BlueMonkMN
A: 

BlueMonkMN - I went ahead and took out the event subscription and get the same result. The full program is now listed below:

 const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";

 // Starting out with a  version of MyLibrary.dll which only has 1 method, named Foo()
 AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
 AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
 appDomain.Load(assemblyName);
 AppDomain.Unload(appDomain);

 // Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
 AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
 AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
 Assembly asm2 = appDomain2.Load(assemblyName2);

 foreach (Type type in asm2.GetExportedTypes())
 {
    foreach (MemberInfo memberInfo in type.GetMembers())
    {
       string name = memberInfo.Name;
       // Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
    }
 }

Seems like there should be no way the assembly is getting pulled into the main app domain between these 2 lines:

appDomain.Load(assemblyName); 
AppDomain.Unload(appDomain);
Jim C
I have run this locally and it works. I suspect that the details of how you swap out the versions, and how those versions are built are going to be the key. Do you just copy one over the other? Are the assemblies strong named?
Mike Two
Actually I got it to fail. I rebuilt the dll to load with a different version number and windows wouldn't even let me swap them out. It said the file was locked. I've written some code that works and I'll put it in an answer. comments aren't good for code.
Mike Two
+1  A: 

I tried to replicate this. In the dll to load (MyLibrary.dll) I built two versions. The first had one class with one method named Foo and had a version number of 1.0.0.0. The second had the same class but the method had been renamed Bar (I'm a traditionalist) and a version number of 2.0.0.0.

I put a breakpoint after the unload call. Then I tried to copy the second version on top of the first version. I assume that is what you are doing because the path never changes. Windows would not let me copy version 2 over version 1. The dll was locked.

I changed the code to load the dll using code executed inside the AppDomain by using DoCallback. That worked. I could swap the dll's and find the new method. Here is the code.

class Program
{
    static void Main(string[] args)
    {
        AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
        appDomain.DoCallBack(loadAssembly);
        appDomain.DomainUnload += appDomain_DomainUnload;

        AppDomain.Unload(appDomain);

        AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
        appDomain2.DoCallBack(loadAssembly);
    }

    private static void loadAssembly()
    {
        string fullPath = "LoadMe1.dll";
        var assembly = Assembly.LoadFrom(fullPath);
        foreach (Type type in assembly.GetExportedTypes())
        {
            foreach (MemberInfo memberInfo in type.GetMembers())
            {
                string name = memberInfo.Name;
                Console.Out.WriteLine("name = {0}", name);
            }
        }
    }

    private static void appDomain_DomainUnload(object sender, EventArgs e)
    {
        Console.Out.WriteLine("unloaded");
    }
}

I did not strong name the assemblies. If you do you are likely to find the first one cached. You can tell by running gacutil /ldl (List download cache) from the command line. If you do find it cached run gacutil /cdl to clear the download cache.

Mike Two
A: 

Mike Two - Thanks for your persistence...loading the assembly from within DoCallBack was the secret sauce. For anyone interested, here is some more info that might be useful down the road:

Sounds like no one could actually reproduce my conditions exactly. To demonstrate the original problem, I generated my dlls this way: 1. Added class library project to solution 2. Built version 1.0.0 with Foo; renamed resulting assembly as MyLibrary.dll.f. 3. Renamed Foo to Goo and built another version 1.0.0; renamed resulting assembly as MyLibrary.dll.g. 4. Removed project from solution. Before starting the run, I removed the .f and ran to breakpoint (first line after the unload). Then I tacked the .f back on and removed the .g from the other dll and ran to the next breakpoint. Windows didn't stop me from renaming. Note: Although it would be better practice, I didn't change the version number because I didn't want to assume my customers always would, since the default entry in AssemblyInfo isn't the wildcard version. It seemed like the uglier case to handle.

Also, I just now discovered something that would have clued me in sooner:

 AssemblyName assemblyName = AssemblyName.GetAssemblyName(FullPath);
 AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
 appDomain.Load(assemblyName);
 Assembly[] tempAssemblies = appDomain.GetAssemblies();
 // MyLibrary.dll has been loaded into the temp domain...good
 Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
 // MyLibrary.dll has been loaded into the main domain, too...bad!

So I'm not sure what the point of AppDomain.Load is, but it seems to have the bonus side effect of loading the assembly into the main app domain as well. Using this experiment, I could see Mike Two's solution cleanly loads it only into the temp domain:

 AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
 appDomain.DoCallBack(CallBackDelegate);  // Executes Assembly.LoadFrom
 Assembly[] tempAssemblies = appDomain.GetAssemblies();
 // MyLibrary.dll has been loaded into the temp domain...good
 Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
 // MyLibrary.dll has NOT been loaded into the main domain...great!

So, Mike Two, exactly how does this StackOverflow newbie mark your answer as "accepted"? Or can I not do that since I'm only a guest here?

Now I'm off to learn how to actually use MyLibrary without sucking the assembly into the main app domain. Thanks everyone for participating.

Jim C
You're right. Renaming the dll worked when copying didn't. Interesting. Sorry, but I don't know how a guest accepts an answer. Glad it was helpful though.
Mike Two
I went ahead and got an OpenID so I could create a StackOverflow account. My account recognizes that I've "answered" 1 question but not that I actually asked one, so I still can't see a way to select your answer, nor can I vote it up, as it seems I have no reputation (at least not here :) Anyway, thanks again.
Jim C
Jim C. It seems that the identity you asked the question under and the identity you answered under are different. That is why you can't accept the answer.
Mike Two
Actually, I was a guest both when I asked and posted answers. I only registered when I thought it would enable me to accept. Registering seems to have found only the cookie created during my answering sessions (thus it upated my sceen name on only those). Oh well. Glad to have found StackOverflow.
Jim C