.NET Fusion
Home About Workshops Articles Writing Talks Books Contact

8. Loading Assemblies Dynamically

Earlier in this workshop I mentioned that libraries are static linked, but are delay loaded. That is, the assembly that uses a library contains the full name of the library, but the library is only loaded just before the calling assembly first uses a type in the library. If the library does not have a strong name it can be located in the application folder, or in a folder under the application folder. If the library has a strong name then it can be in the application folder, in any folder on the local disc, in the GAC or on another machine (and referenced through a URI).

The .NET framework allows you to load an assembly in code. You will rarely want to do this because it means that the metadata for the library will not be available at compile time. So the metadata of the types and members that you want to access will not be stored in the calling assembly. You will not be able to use the language to activate the type with new, nor will you be able to call members of the type directly. Instead, you'll have to use the framework activation classes to create the object and use reflection to access the object's members. This is equivalent to Visual Basic and VBA late binding, and it has the same problems as those technologies. With early binding you get the compiler to perform type checks at compile time and inform you, the developer of type mismatches so that you can change the code; with late binding the type checks are performed at runtime by the user of the code, your customer. If you are happy to enlist your customers into the development process, then go ahead and use late binding.

Note that libraries are loaded into an application domain and although there is a mechanism to load a library there is no mechanism to unload an assembly. However, there is a mechanism to unload an entire application domain (and hence, all the assemblies loaded into it). There are some interesting issues that occur when you load libraries dynamically.

8.1 Load and Invoke Through Reflection

Use the lib.cs that has been used throughout the workshop, use the version that has a strong name (and uses a key file) and is versioned with version 1.0.0.0. The process assembly looks like this:

using System;
using System.Reflection;

class App
{
   static void Main()
   {
      try
      {
         object obj = FromAssembly("lib");
         InvokeObject(obj);
      }
      catch(Exception e1)
      {
         Console.WriteLine(e1.Message);
      }
   }
   static object FromAssembly(string shortName)
   {
      Console.WriteLine("Use Assembly.Load");
      AssemblyName name = new AssemblyName();
      name.Name = shortName;
      Assembly a = Assembly.Load(name);
      Type type = a.GetType("LibraryCode");
      ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
      return ctor.Invoke(null);
   }
   static void InvokeObject(object obj)
   {
      Type type = obj.GetType();
      MethodInfo mi = type.GetMethod("GetVersion");
      string version = (string)mi.Invoke(obj, null);
      Console.WriteLine(version);
   }
}

In this code the FromAssembly method loads an object and InvokeObject method runs the GetVersion method on that object. In this code FromAssembly generates the assembly name and then uses Assembly.Load to load the assembly. Compile the library and the process; run the process and you'll see that the library will be loaded from the local folder. This is acceptable because it is not possible to have two libraries with the same short name but with different versions or public key token in the same Win32 folder. For private assemblies Fusion only checks the short name (the culture part is handled in a different way, and this will be investigated later).

For future reference, obtain the public key token from the library:

C:\TestFolder>sn -T lib.dll

Microsoft (R) .NET Framework Strong Name Utility Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Public key token is 3bf941bb1f722efe

Now add the library to the GAC and delete the local copy of the assembly. Run the application. You'll find that a FileNotFoundException will be thrown. The reason is that when you request an assembly from the GAC with Assembly.Load Fusion will require the complete name of the assembly.

Now change the FromAssembly method to include the complete name:

static object FromAssembly(string shortName)
{
   Console.WriteLine("Use Assembly.Load");
   AssemblyName name = new AssemblyName();
   name.Name = shortName;
   name.Version = new Version("1.0.0.0");
   name.CultureInfo = new CultureInfo("");
   byte[] pkt
      = new byte[] {0x3b,0xf9,0x41,0xbb,0x1f,0x72,0x2e,0xfe};
   name.SetPublicKeyToken(pkt);

   Assembly a = Assembly.Load(name);
   Type type = a.GetType("LibraryCode");
   ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
   return ctor.Invoke(null);
}

The array, pkt, contains the bytes you recorded earlier. In this case the assembly does not have a culture, however, this is treated as being a neutral culture which is why the CultureInfo property has to be initialised with a CultureInfo object that has an empty string in its constructor. (To use CultureInfo you will have to add a using statement for System.Globalization.)

Compile just the process and run it. Now you'll find that the library will be loaded from the GAC. There is an overload of Assembly.Load that takes a string and if you provide the short name of an assembly (for example lib) then the library will be loaded only if it is a private assembly. If you know the full name of the assembly you can get an assembly from the GAC:

Assembly a = Assembly.Load("lib, Version=1.0.0.0,"
   + " Culture=neutral, PublicKeyToken=3bf941bb1f722efe");

The results of the last example is acceptable, after all, you are asking Fusion to load an assembly in the cache and so it is a reasonable requirement to have to give the complete name for the assembly. There are other ways to load an assembly:

Assembly a1 = Assembly.LoadFile(@"c:\TestFolder\lib.dll");
Assembly a2 = Assembly.LoadFrom("lib.dll");

LoadFile does not use Fusion to find a file. Instead, you provide a full path for this method. This means that you can use this method to load an assembly from any location, for example, from the GAC if you know the full Win32 path to the file. Of course, determining the right path to get a file out of the GAC is just what Fusion is for! LoadFrom can also take a full path, but it can also take a relative path.

The problem with these methods is that if we only know the short name of the assembly that assembly must be a private assembly because we cannot provide enough information to get the assembly from the GAC.

In addition, Load is also overloaded to take a byte[] array. The array contains the actual bytes of the assembly that you want to load. This overload is independent of the file system so you can load any valid assembly this way, as long as it has been serialized to a byte[].

To see how this works create a local copy of lib.dll. Add the following method to your code:

static object FromFile(string fileName)
{
   System.IO.FileStream fs = System.IO.File.OpenRead(fileName);
   byte[] data = new byte[fs.Length];
   fs.Read(data, 0, data.Length);
   fs.Close();

   Assembly a = Assembly.Load(data);
   Type type = a.GetType("LibraryCode");
   ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
   return ctor.Invoke(null);
}

Now add the following line to the Main method:

object obj = FromFile("lib.dll");
InvokeObject(obj);

Notice that the full name of the file has to be provided for File.OpenRead. As you've learned earlier Fusion only needs the short name because it will append .dll and .exe to the short name to get the file name.

.NET Version 3.0
The Assembly class has two new methods, ReflectionOnlyLoad and ReflectionOnlyLoadFrom. These two will return the assembly so that you can inspect the code with reflection, but you will not be able to execute that code.

Now remove the FromFile and the call to this method before moving on to the next example.

8.2 Activating Objects

The other main problem with the Assembly functions is that you have to activate the object through reflection. There are two other ways to activate an object, here's one:

static object UseActivator(string name)
{
   ObjectHandle oh
      = Activator.CreateInstance(name, "LibraryCode");
   return oh.Unwrap();
}

Add this to the process and replace the call to FromAssembly to a call to UseActivator. (Also add a using statement for System.Runtime.Remoting) Compile both the process and the library and run the process. You'll find that the library will be loaded. Now replace the name of the assembly with:

lib, Version=1.0.0.0, Culture=neutral,
   PublicKeyToken=3bf941bb1f722efe

(Remember to use your own public key token.) Delete the local library and run the code. You'll find that the activator will obtain the assembly from the GAC and activate the requested object. Activator is used by remoting and ObjectHandle represents a mechanism to pass object references between application domains. However, as you can see above you have to unwrap the ObjectHandle before you can use the object. The AppDomain has a method that will activate an object and unwrap the handle:

static object UseAppDomain(string name)
{
   AppDomain ad = AppDomain.CurrentDomain;
   return ad.CreateInstanceAndUnwrap(name, "LibraryCode");
}

Add this method and replace the call to UseActivator with a call to UseAppDomain. Compile the process and confirm that you can activate types from assemblies in the GAC.

8.3 Local and GAC Assemblies

We will do just one more experiment with this code. The Main method should call UseAppDomain with a full name, add another call to this method with a short name:

static void Main()
{
   object obj = null;
   try
   {
      obj = UseAppDomain("lib, Version=1.0.0.0, "
         + "Culture=neutral, PublicKeyToken=3bf941bb1f722efe");
      InvokeObject(obj);
   }
   catch(Exception e1)
   {
      Console.WriteLine(e1.GetType().ToString());
   }
   try
   {
      obj = UseAppDomain("lib");
      InvokeObject(obj);
   }
   catch(Exception e2)
   {
      Console.WriteLine(e2.GetType().ToString());
   }
}

Of course, add your own public key token. Compile the process and the library. First, ensure that the library is not in the GAC (gacutil -u lib). Run the process. You'll find that the local copy of the library is picked up by both calls. This is understandable because both of the names refer to the local library.

Now add the library to the GAC (gacutil -i lib.dll) and rename the local file (rename lib.dll lib.old). Run the process. Now you'll find that the call with the full name will get the library from the GAC, but the call with the short name will fail with FileNotFoundException. Again, this is understandable, when given a full name Fusion is able to search the GAC for the library, but with a short name only the application folder can be used and this should fail because an appropriate file cannot be found. (Note that it is slightly more complicated than this.) Remember, Fusion will append .dll or .exe to the short name.

Now retrieve the local library (rename lib.old lib.dll). You now have a copy in the GAC and a local copy, the call with the full name should obtain the GAC library, the call with the short name should be able to get the local version. Run the process. What do you see? In fact, both calls (full name and short name) will get the library from the GAC. This is surprising, because it indicates that when given a short name Fusion will try to get a local version, and if it does it uses information in the local version to get the full name and then tries to get the assembly from the GAC. What's happening here?

Well, this is how Load is meant to work. The mechanism is this. First the method checks with application folder for the assembly, to do this it checks for a local codeBase in the configuration file and if there is one then this subfolder is checked.

If the assembly is specified as having a culture the checks are then performed under the subfolder with the culture name, otherwise the assembly is searched for in the application folder. The routine first checks for a DLL with the short name and if that is not found, it checks a subfolder with the short name for the DLL. If the assembly has not been found, then an EXE will be searched for (first in the folder, then in a subfolder with the assembly short name). Then Fusion checks to see if there is a privatePath in a <probing> element, if there is one, Fusion checks these folders for a DLL or EXE, both in the specified folders and in subfolders with the short name. But, of course, you know all of this already.

Now, in our case the library has a strong name, and the library is both in the GAC and in the application folder. The steps above will load the private assembly. However, Load sees that the library has a strong name, so it then uses this information to search for the assembly, that is, it will check application configuration, publishing policy and machine configuration to see if there is a version redirect. It then checks to see if the assembly has already been loaded and if not, it checks the GAC for the assembly. If the assembly is found in the GAC that version is returned, otherwise the local version is returned.

8.4 Partial Names

The Assembly class also allows you to load an assembly using a partial name through the LoadWithPartialName method.

From the last example you should have version 1.0.0.0 of lib in the GAC (check this with gacutil -l lib, and if it is not there, add it). Now change the library to make it version 1.1.0.0, compile it and add it to the GAC, then delete the local copy. Add the following method to the process:

static object UsePartialName(string name)
{
   Assembly a = Assembly.LoadWithPartialName(name);
   Type type = a.GetType("LibraryCode");
   ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
   return ctor.Invoke(null);
}

Change Main so that it calls UsePartialName like this:

object obj = UsePartialName("lib");
InvokeObject(obj);

Compile and run the process.

.NET Version 3.0
The LoadWithPartialName is deprecated in .NET version 3.0/2.0, mainly because loading using partial names is a risky action, as will be explained later. You should use the Load method instead. When you compile this code you will get a warning CS0618.

On my machine this will return:

version=1.1.0.0 codebase=file:///c:/windows/assembly/gac/lib/
   1.1.0.0__3bf941bb1f722efe/lib.dll

This looks like Fusion is loading the latest version, but this is not the case. Let's imagine that we want to use the first version of the library. Change the method call to:

object obj = UsePartialName("lib, Version=1.0.0.0");

Compile and run the process. You'll find that version 1.1.0.0 is still picked up. Earlier on in the workshop I mentioned that Fusion will not do versioning without a public key token, this applies to the actual libraries and to the names passed to LoadWithPartialName. If you do not provide a public key token then Fusion will return the first library that it finds. Change the method to contain the public key token.

object obj = UsePartialName(
   "lib, Version=1.0.0.0, PublicKeyToken=3bf941bb1f722efe");

Compile and run. Does this make a difference? Well, although versioning is now enabled Fusion still returns the first assembly it can find in the GAC. The reason for this is because there is an order of preference in partial names:

name, culture, public key token, version

As you add more information you must add it in this order. So to turn on versioning you have to have the public key token and a culture. This means that if you want to pick up a particular version you must provide a full name. However, the documentation does mention that if you miss off the version then Fusion will always return the highest version.

This last statement shows a distinct problem with partial names. To get full versioning you have to give the full name so you may as well use Assembly.Load (which is the recommendation in .NET version 3.0/2.0). If you provide less information then Fusion will always load the latest version, but how do you know that the latest version is compatible with the application requesting the library? One insidious issue with this, is that an application could install a later version of a library and this could break other applications installed on the machine that used earlier versions of the library. This is a return to DLL Hell and Microsoft has recognised this and have taken steps to remove this issue.

8.5 <qualifyAssembly>

There is one other way to handle partial names and this involves a configuration file setting. You can add a <qualifyAssembly> element in the <assemblyBinding> element. This allows you to match a partial name with a specific full name. Unfortunately, the configuration tool does not have a tab to do this, so you have to make the changes by hand. Create a file called app.exe.config in notepad and add the following:

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <qualifyAssembly
        partialName="lib"
        fullName="lib,version=1.0.0.0,culture=neutral,
           publicKeyToken=3bf941bb1f722efe"
      />
    </assemblyBinding>
  </runtime>
</configuration>

The fullName should be on a single line. Change the call to UsePartialName so that it looks like this:

object obj = UsePartialName("lib");

Now run the process. You'll find that version 1.0.0.0 is picked up as specified in the configuration file. The idea is that you can make your calls to dynamically load assemblies use partial names and then provide the full information in the configuration file when you know exactly what assembly should be loaded.

.NET Version 3.0
If you replace the call to LoadWithPartialName with a call to Load the <qualifyAssembly> can be used to give a fully qualified name for the assembly.

 Clean up the example by removing the libraries from the GAC (gacutil -u lib).

8.6 AssemblyResolve Event

If you attempt to load an assembly and Fusion cannot find the specified assembly it will raise the AssemblyResolve event for the current AppDomain. This event is odd compared to most .NET events because it returns a value and hence it means that only one delegate should handle the event. If you have more than one event handler then the return value from the last event handler that is called will be used which makes the other event handlers rather pointless. The AssemblyResolve event is a ResolveEventHandler delegate:

public delegate Assembly ResolveEventHandler(
   object sender, ResolveEventArgs args);

The delegate takes a ResolveEventArgs which has a single string property called Name that has the full name for the assembly that was requested (after the various policies have been applied). The event handler should use this name to locate and load the requested assembly. Bear in mind that at this point Fusion has been unable to locate the assembly so in most cases you should not use a method that uses Fusion to load the assembly.

As an example, use the library (and key file) from the last example and change the application to use FromAssembly and InvokeObject as you used earlier. The Main method should look like this:

static void Main()
{
   AppDomain.CurrentDomain.AssemblyResolve
      += new ResolveEventHandler(ResolveAssembly);
   try
   {
      object obj = FromAssembly("lib");
      InvokeObject(obj);
   }
   catch(Exception e)
   {
      Console.WriteLine(e.GetType().ToString());
   }
}

The event handler should attempt to load the assembly, for the time being just implement it to do nothing:

static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
{
   return null;
}

Now rename the library to lib.old so that Fusion cannot find it (ren lib.dll lib.old). Run the application and you'll find that a FileNotFoundException will be thrown. This is understandable because although you provide complete information about the assembly name, no assembly with the short name can be found in the local folder, and no assembly with the full name can be found in the GAC. However, we know that the requested assembly is in the local folder, it's merely been renamed, so you can use the event handler to give the real name:

static Assembly ResolveAssembly(
   object sender, ResolveEventArgs args)
{
   string[] name = args.Name.Split();
   string assem = name[0].Substring(0, name[0].Length-1);
   return Assembly.LoadFrom(assem + ".old");
}

The Name property will give the full name, so the string up to the first space will be lib, (note the comma), hence we need to strip the comma and then add the extension .old. This code works as expected, it loads the assembly and then calls the requested type.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Nine

This page is (c) 2007 Richard Grimes, all rights reserved