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.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:
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:
{
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:
+ " 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 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:
{
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:
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:
{
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:
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:
{
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:
{
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:
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:
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:
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.
"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:
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:
<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:
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:
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:
{
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:
{
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:
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.