Chapter 7. Loading Assemblies in Extensible Applications

You’ve seen how application domains can be used to isolate groups of assemblies within a process and have taken a look at the techniques available to customize domains for various scenarios. In this chapter, I discuss what’s involved in loading assemblies into the application domains you create as part of your extensible application. Much has been written in various books, magazines, and product documentation about the general topic of assembly loading. Rather than repeating it, I focus on those aspects of assembly loading that are of specific interest to writers of extensible applications.

By their nature, extensible applications don’t have upfront knowledge of which assemblies will be loaded while the application is running. Instead, the set of add-ins to be loaded is typically specified either interactively by the end user or through some sort of dynamic configuration system. For example, the user of a productivity application can have the option to choose an add-in to run using a menu command. Similarly, a Microsoft SQL Server administrator has the option of adding new assemblies to a database at any time. In addition, it’s often the case that the assemblies loaded into an extensible application are written by a variety of different vendors.

The dynamic nature of extensible applications makes assembly loading more complicated for two primary reasons. First, assemblies are typically loaded in a late-bound fashion. In more static applications, the majority of assembly references are recorded in an application’s metadata when the application is compiled. The CLR reads these references as the application is running and follows a well-defined set of rules for locating and loading the assembly. In contrast, the add-in assemblies in extensible applications are loaded using a variety of APIs, some of which are managed and some of which are unmanaged. Not only can you use numerous APIs, you can use different basic techniques to identify the assembly you’d like to load. You can specify a reference to an assembly dynamically either by supplying the assembly’s identity (or even just part of its identity) or by providing the fully qualified name of a disk file that contains the assembly’s manifest. The second reason that assembly loading is more complicated in dynamic applications is CLR and Microsoft .NET Framework versioning. Because the origin of the assemblies you’ll load into an extensible application varies, there’s a good chance that not all assemblies will be built using the same version of the CLR and the .NET Framework. As times goes on and the number of publicly available versions of the .NET Framework increases, it will become more and more likely that you’ll load assemblies with varying version dependencies into the same process. Understanding the implications of loading assemblies built with different versions of the .NET Framework is necessary not only to ensure that add-ins work predictably, but also to ensure that the stability of your overall application isn’t compromised.

Concepts and Terminology

Before I get into the details of how to load assemblies into extensible applications, let me take a step back and outline some basic concepts and terminology. The topic of assembly loading is laden with new terms that are sometimes overloaded when they shouldn’t be. To describe how to reference and load assemblies, it’s best to agree on a consistent set of terminology. In the next few sections I describe the concepts and terminology that I use throughout the rest of this chapter. In particular, I define the following:

  • Strong versus weak assembly names

  • Early-bound versus late-bound references

  • Fully specified versus partially specified references

  • Version policy

Strong and Weak Assembly Names

A .NET Framework assembly is said to have either a weak name or a strong name. Throughout this chapter, I demonstrate how the factors you must consider when loading an assembly are different based on whether the assembly has a strong name or a weak name. For example, I’ll show that the CLR uses different rules for locating an assembly when referring to it by its strong name rather than its weak name.

An assembly has a strong name if it has been signed with a cryptographic key pair using either a compiler or the sn.exe SDK tool. If an assembly has not been signed, it has a weak name. Structurally, assemblies with strong names are different from assemblies with weak names in two key ways:

  1. Assemblies with strong names have a digital signature embedded in the file containing the manifest.

  2. The name of strong-named assemblies contains the public key used to generate the signature.

This second characteristic is particularly important to the process of referencing and loading an assembly.

An assembly’s identity consists of four parts:

  • Friendly name

  • Version

  • Public key

  • Culture

Assemblies with weak names have a friendly name, a version, and an optional culture. Assemblies with a strong name have a friendly name, a version, a public key, and a culture. You can see this difference in the portion of an assembly’s metadata that records its name. In the following listing, I’ve used the ildasm.exe SDK tool to display a portion of the metadata for an assembly with a strong name. Note the presence of a public key in the name.

.assembly BoatRaceHostRuntime
{
  .publickey = (00 24 00 00 04 80 00 00 94 00 00 00 06 02 00 00
                00 24 00 00 52 53 41 31 00 04 00 00 01 00 01 00
                07 D1 FA 57 C4 AE D9 F0 A3 2E 84 AA 0F AE FD 0D
                E9 E8 FD 6A EC 8F 87 FB 03 76 6C 83 4C 99 92 1E
                B2 3B E7 9A D9 D5 DC C1 DD 9A D2 36 13 21 02 90
                0B 72 3C F9 80 95 7F C4 E1 77 10 8F C6 07 77 4F
                29 E8 32 0E 92 EA 05 EC E4 E8 21 C0 A5 EF E8 F1
                64 5C 4C 0C 93 C1 AB 99 28 5D 62 2C AA 65 2C 1D
                FA D6 3D 74 5D 6F 2D E5 F1 7E 5E AF 0F C4 96 3D
                26 1C 8A 12 43 65 18 20 6D C0 93 34 4D 5A D2 93 )
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}

In contrast, the following listing shows the metadata stored for an assembly with a weak name:

.assembly Utilities
{
.ver 1:0:0:0
}

Assemblies are given strong names when you intend them to be shared among several applications on the system. Shared code has much more stringent requirements than code that is private to only one application. These additional requirements are primarily related to security and versioning. For example, assemblies used by many applications require a robust approach to versioning to avoid the versioning conflicts associated with Win32 DLLs. New versions of the shared assembly must be able to be added to the system without breaking applications that depend on previous versions. A cryptographically strong name is required to ensure that a given assembly can’t be altered by anyone other than the original assembly author.

For a more thorough description of assembly names, including more discussion on the motivation behind using strong names, see Chapter 2 and Chapter 3 in Applied Microsoft .NET Framework Programming (Microsoft Press, 2002) by Jeffrey Richter.

Early-Bound and Late-Bound References

Assemblies can be referenced in either an early-bound or a late-bound fashion. Early-bound references are recorded in metadata when an assembly is compiled. Late-bound references are specified on the fly using the APIs on such classes as System.Reflection.Assembly, System.Activator, or System.AppDomain.

Extensible applications are likely to use both early- and late-bound references. Assemblies that are part of the implementation of the extensible application are likely to be referenced using early binding. In contrast, the add-ins that are dynamically added to the application to extend it are loaded using late binding because the application doesn’t know which add-ins it will load (and, hence, can’t reference them) when the application is compiled.

Let’s return to the boat race example introduced in Chapter 5 to illustrate more concretely how these two types of references are likely to be used in an extensible application. Assume that our boat race application is written entirely in managed code and consists of a main executable called boatracehost.exe and three utility DLLs called boatracehostruntime.dll, weather.dll, and sailconfigurations.dll. When boatracehost.exe is compiled, it statically references the utility DLLs using the /r compiler switch, like so:

C:	empBoatRaceHost>csc /target:winexe /
r:BoatRaceHostRuntime.dll, SailConfigurations.dll,Weather.dll BoatRaceHost.cs

After compiling boatracehost.cs using this command line, the manifest for boatracehost.exe looks like this (produced with ildasm.exe):

.assembly BoatRaceHost
{
  .ver 1:0:0:0
}
.assembly extern BoatRaceHostRuntime
{
  .publickeytoken = (4B 7D 48 01 D8 95 67 95)
  .ver 1:0:0:0
}
.assembly extern SailConfigurations
{
  .publickeytoken = (4B 7D 48 01 D8 95 67 95)
  .ver 1:0:0:0
}
.assembly extern Weather
{
  .publickeytoken = (4B 7D 48 01 D8 95 67 95)
  .ver 1:0:0:0
}
.assembly extern System.Windows.Forms
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 1:0:5000:0
}
.assembly extern System
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 1:0:5000:0
}
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 1:0:5000:0
}
  .assembly extern System.Drawing
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A)
  .ver 1:0:5000:0
}

Each .assembly extern statement in the previous listing represents an early-bound reference to an assembly. Notice that in addition to the references to SailConfigurations, Weather, and BoatRaceHostRuntime, our application has early-bound references to the .NET Framework assemblies it uses, including System.Drawing and System.Windows.Forms.

To illustrate when late-bound references are used, let’s assume our boatracehost application includes a menu command that enables users to add boats to the race dynamically. During a particular run of the application, the end user adds two boats to the race. These two boats are contained in the assemblies Alingi and TeamNZ. Both Alingi and TeamNZ are loaded dynamically; hence, late binding is used as shown in Figure 7-1.

Early- and late-bound assembly references in extensible applications

Figure 7-1. Early- and late-bound assembly references in extensible applications

This chapter focuses primarily on late binding because the flexibility introduced by loading assemblies on the fly introduces several considerations unique to extensible applications. In particular, the fact that late-bound references can be partially specified introduces complexities you don’t run into when all assembly references are early bound.

Fully Specified and Partially Specified References

As described, assemblies in .NET Framework are identified by a friendly name, a public key, a version number, and a culture. When referencing an assembly, you can specify all four of these fields or you can specify just the friendly name—the public key, version number, and culture are all optional. When all four fields are specified, the assembly reference is said to be fully specified. If anything less than all four fields is included, the reference is partially specified.

Partially specified references are unique to late binding. When a compiler records an earlybound reference to an assembly in metadata, it stores values for all four fields. These values can be null (as the value for the public key is when an assembly has a weak name), but they are values nonetheless. In contrast, the APIs that enable you to load an assembly dynamically allow you to omit values for the public key, version number, and culture fields. Referencing an assembly by only a portion of its name results in looser binding semantics than a fully specified reference does. For example, if you want to load an assembly by its friendly name only, without regard to version, you can simply omit a version number from your reference. If the CLR finds an assembly whose friendly name matches, it loads it regardless of which version it is. These looser binding semantics are useful in some scenarios. Microsoft ASP.NET, for example, uses partial binding and weakly named assemblies to implement loose version binding for assemblies stored in the bin subdirectory under the application’s root directory. You might have noticed you can place any assembly in the bin directory and, as long as its name matches, it is loaded, regardless of version.

Although the flexibility allowed by partially specified assembly references is useful in scenarios like these, you should also be aware of its complexities. For example, the rules for which of the four fields must match are different depending on whether the assembly that matches the friendly name has a strong name or a weak name. I discuss these complexities in the section "Partially Specified Assembly References" later in this chapter.

Version Policy

As described, strong names are part of the infrastructure the CLR uses to provide a system that employs strict version checking to minimize inadvertent conflicts between different versions of an assembly. By default, the CLR loads the exact version of the assembly you specify in your reference. However, this version can be redirected to a different version through a series of statements called version policy. Version policy is useful in several scenarios. For example, a machine administrator can specify version policy that would prevent anyone on the machine from using a version of an assembly that is known to have security vulnerabilities or other serious bugs. In addition, an application can use version policy to load a version of a .NET Framework assembly different from the default.

These version policy statements are specified in Extensible Markup Language (XML) files and are most easily created with the .NET Framework Configuration tool. The following is an example of a version policy statement that causes version 6.0.0.0 of an assembly whose friendly name is Alingi to be loaded, regardless of which version was specified:

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  <dependentAssembly>
    <assemblyIdentity name="Alingi" publicKeyToken="ae4cc5eda5032777" />
      <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535"
        newVersion="6.0.0.0" />
  </dependentAssembly>
</assemblyBinding>

Version policy can be specified at three different levels. First, the author of an application that uses shared assemblies can specify version policy in the application configuration file. Second, the publisher of a shared assembly can provide version policy in what is called a publisher policy assembly. Finally, the machine administrator can specify version policy through entries in the machine.config file. When the CLR resolves a reference to a strong-named assembly, it begins by looking in these three locations for version policy statements that might redirect the assembly reference to a different version. The three policy levels also form a hierarchy in that they are evaluated in sequence with the output of one level feeding into the next. For example, say an application configuration file contains a policy statement that redirects all references to an assembly from version 1.0.0.0 to version 2.0.0.0. If the policy statement in the application configuration file applies to the reference being resolved, the CLR next evaluates publisher policy looking for any policy statements that redirect version 2.0.0.0 of the assembly. Likewise, administrator policy is evaluated based on the outcome of the publisher policy phase.

This brief description of version policy gives you what you need to understand the concepts presented in this chapter. For more details on how version policy is specified and resolved, see Chapter 2 and Chapter 3 in Applied Microsoft .NET Framework Programming (Microsoft Press, 2002) by Jeffrey Richter.

Loading Assemblies by Assembly Identity

The .NET Framework class libraries provide several APIs you can use to load an assembly dynamically by assembly identity. Throughout this chapter, I refer to these APIs, as well as the APIs that enable you to load an assembly given a filename, as the assembly loading APIs. The assembly loading APIs that take an assembly identity enable you to specify an assembly’s identity in one of two ways. First, you can supply the identity as a string that follows a welldefined format. Or you can supply an instance of the System.Reflection.AssemblyName class. This class contains properties for the textual name, public key, culture, and version components of the assembly identity. In this section I describe how to use the assembly loading APIs to load add-ins into an extensible application dynamically.

Before I get into the details of how to call these APIs, however, it’s worth taking a step back and revisiting the extensible application architecture introduced in Chapter 1. Making the most effective use of the assembly loading APIs involves more than just knowing the details of how to call the APIs. Your extensible application will have a much cleaner design and will perform much better if you think through how your use of application domains relates to assembly loading. This involves understanding your application domain boundaries and taking advantage of the application domain manager infrastructure discussed in Chapter 5. By looking back at the basic architecture, you can see how best to take advantage of the assembly loading APIs without introducing unintended side effects such as assemblies loaded into the wrong application domain.

After I’ve discussed how the assembly loading APIs fit into the overall architecture of an extensible application, I cover the details involved in calling these APIs. In addition to looking at the APIs themselves, I discuss briefly the CLR’s rules for locating assemblies and the impact of partially specified references.

Architecture of an Extensible Application Revisited

In Chapter 1, I introduced the typical architecture of an extensible application. In the last few chapters, I’ve made this architecture more concrete by describing the role that application domains play in applications that are extensible. Application domains exist for one purpose: as containers for assemblies. The main goal of this architecture is to provide the infrastructure in which to load assemblies dynamically. Let me review this architecture now and highlight the key design points that affect how the add-in assemblies are loaded (see Figure 7-2). The key points include the following:

  • Multiple application domains are used. Extensible applications typically create multiple application domains to isolate add-ins (or groups of add-ins) from others loaded in the same process. It’s important to call the assembly loading APIs from the domain in which an add-in is to be loaded. This results in the cleanest design and the best overall performance.

  • An application domain manager is created for each domain. In Chapter 5 I introduced the notion of an application domain manager. An application domain manager is a convenient place from which to call the assembly loading APIs because the CLR automatically creates an instance of your application domain manager in each new application domain rather than requiring you to write code to load your domain manager explicitly into each new domain you create.

  • Add-in assemblies are not loaded in the default application domain. Most extensible applications avoid loading add-in assemblies into the default application domain primarily because the default domain cannot be unloaded without shutting down the entire process. As a result, you typically see just the application domain manager class loaded into the default domain. From there, other domains are created to contain the add-ins.

  • Communication between application domains is limited to the application domain managers. In Chapter 5 I discuss how to design your application to make the most effective use of application domains. One design goal is to limit communication between application domains as much as possible. This includes two aspects. First, the volume of communication should be limited both in terms of the number of calls made and the amount of data exchanged by those calls. Limiting the volume of communication helps your application perform better because less cross-domain marshaling is required. The other aspect of cross-domain communication that you aim to limit is the number of assemblies involved in calls across application domains. If an assembly is to participate in a call between two application domains, that assembly must be loaded into both application domains. To be loaded into two application domains, an assembly must be deployed in such a way as to be visible to both domains. Furthermore, both domains must be unloaded to remove the assembly from the process completely. In short, the fewer assemblies that are involved in cross-domain communication, the better, from the perspective of both performance and ease of deployment. Because an application domain manager is automatically loaded into each new domain for you, it must already be deployed in such a way that it is visible to multiple application domains. So it’s natural to use application domain managers to communicate across application domains.

The architecture of an extensible application

Figure 7-2. The architecture of an extensible application

As described, the primary goal to keep in mind when designing your assembly loading infrastructure is always to call the assembly loading APIs from the application domain in which you intend the add-in assembly to be loaded. This design is shown in Figure 7-2 by the calls to AppDomain.Load originating in the application domain manager and resulting in the add-in assembly being loaded into the same domain. To get a clear picture of why this design goal is desirable, take a look at how assemblies are represented in the .NET Framework class libraries and how that representation relates to the CLR’s infrastructure for calling methods on a class in a different application domain.

System.Reflection.Assembly and CLR Remote Calls

Recall from Chapter 5 that calling a method on a type in another application domain is a remote call. The mechanics for a remote call are different depending on the marshaling characteristics of the type you are calling. Generally speaking, types are either considered marshaled by value or marshaled by reference in CLR remoting terminology. Types that are marshaled by reference are those types that derive from the System.MarshalByRefObject base class. When you call a method on a type derived from MarshalByRef in a different application domain, the CLR creates a proxy for that type in the calling application domain. All calls are made through the proxy to the actual type as shown in Figure 7-3.

Calling a MarshalByRefObject in a different application domain

Figure 7-3. Calling a MarshalByRefObject in a different application domain

In contrast, when a call is made to an object in another application domain that is marshaled by value, a copy of the instance is made in the calling domain. All objects that are marshaled by value must be marked with the [Serializable] custom attribute so the CLR knows how to transfer the object into the new domain. All calls on the type are made to the copy instead of through a proxy to the original as shown in Figure 7-4.

Calling a type in a different application domain that is marshaled by value

Figure 7-4. Calling a type in a different application domain that is marshaled by value

At this point, you might be wondering what this discussion about CLR remoting has to do with loading assemblies. This matters because the type used to represent assemblies in the .NET Framework class libraries, System.Reflection.Assembly, is marshaled by value, not by reference. Because instances of System.Reflection.Assembly are copied between application domains, it’s easy to inadvertently end up loading an assembly into an application domain unintentionally. Look at a concrete example to see how easy it is to make this mistake.

In Chapter 5 and Chapter 6, we used a CLR host called boatracehost.exe as an example of how to make effective use of application domains in an extensible application. We continue that example in this chapter as we discuss how to use the assembly loading APIs in conjunction with application domains. As described, you can use several APIs in the .NET Framework class libraries to load assemblies dynamically. AppDomain.Load is one of these methods that enables you to load an assembly into an application domain. Let’s say for purposes of example that a new boat is entering a race hosted by boatracehost. We’d like to load this add-in into a new application domain, so we use AppDomain.CreateDomain to create the new domain. We then call AppDomain.Load to load the boat add-in in the new domain as shown in the following code:

static void Main(string[] args)
{
   AppDomainSetup adSetup = new AppDomainSetup();
   adSetup.ApplicationBase = @"c:Program FilesBoatRaceHostAddins";

   AppDomain ad = AppDomain.CreateDomain("Alingi Domain",
                                          null,
                                          adSetup);

   Assembly alingiAssembly = ad.Load("Alingi, Version=5.0.0.0,
                                     PublicKeyToken=5cf360b40180107c,
                                     culture=neutral");
}

The call to AppDomain.Load in the preceding code is a remote call from the default application domain (in which main() is running) to the new domain held in the variable of type AppDomain named ad. AppDomain.Load takes as input the name of the assembly we’d like to load and returns an instance of System.Reflection.Assembly. Because Assembly is marshaled by value, a copy of the instance of the Assembly type is made in the default domain when the call to App-Domain.Load returns as shown in Figure 7-5.

An assembly inadvertently loaded into two application domains

Figure 7-5. An assembly inadvertently loaded into two application domains

Instances of Assembly contain data describing the underlying assembly they represent. For example, given an instance of Assembly, you can determine the assembly’s name, the assemblies it depends on, and so on. The underlying assembly represented by an instance of Assembly must be loaded into the application domain where the instance resides. In this example, this means that the Alingi assembly must be loaded into both the default application domain and the Alingi Domain. This side effect of calling AppDomain.Load affects our application design in a few key ways. First, the fact that the add-in assembly has been loaded into our default application domain means we can’t unload that assembly from our application without terminating the entire process. This is clearly undesirable from both the perspectives of memory usage and type visibility. Because we can never unload the assembly, we might be stuck dealing with the additional memory it consumes even when we no longer need the assembly within the application. Also, once an assembly is loaded into a given application domain, it can discover all other assemblies in that same domain using the GetAssemblies method on the AppDomain type. Once another assembly is discovered, it can be reflected upon using the types in the System.Reflection namespace. If the code access security policy for that domain isn’t configured to disallow it, code in an add-in assembly could even invoke methods on any other assembly loaded in the same domain.

The other reason loading an add-in into the default application domain affects our design is that it complicates the deployment of the add-in. Recall from Chapter 6 that each application domain has an ApplicationBase that establishes a root directory in which assemblies for that domain can be deployed. In the preceding code sample, the ApplicationBase for Alingi Domain has been set to c:program filesoatracehostaddins. By deploying the Alingi assembly to that directory, it is found by the CLR when we call AppDomain.Load. However, because we’ve also inadvertently added Alingi to the default application domain, the add-in must be deployed to a location where the CLR will find it for that domain as well. This means deploying the add-in to another ApplicationBase or adding it to a global location such as the global assembly cache (GAC). This subtlety often results in unexpected failures to load an assembly. For example, in looking at the previous code, it’s obvious that we need to deploy our add-in to c:program filesoatracehostaddins. However, if we did just that, we’d get a FileNotFoundException telling us that the assembly we’re loading cannot be found. When I see these errors, I typically look in the directory in which I expect the assembly to be found, and, seeing it there, I’m at a loss for a few minutes before I realize that the CLR is trying to load my assembly into an application domain I never intended. Because of all this, it is far better to call the assembly loading APIs from within the domain in which you intend the add-in to be loaded.

Recommendations for Loading Assemblies in Extensible Applications

Most extensible applications leverage the application domain manager concept introduced in Chapter 5 to load add-ins from within the desired application domain. As described, the application domain manager is a natural place from which to initiate assembly loads because the CLR takes care of creating an instance of the application domain manager in each new application domain you create. In leveraging this design, most extensible applications follow a series of steps similar to the following when loading a new add-in to the application:

  1. The extensible application is made aware of the new add-in.

  2. An application domain is chosen in which to load the new add-in.

  3. The application domain manager in the target domain is called to load the add-in.

  4. The application domain manager in the target domain loads the add-in.

These steps are described in the following sections.

Step 1: The Extensible Application Is Made Aware of the New Add-In

The means by which add-ins are introduced to an extensible application are completely up to the application. So there is no general approach to recommend. Instead, I discuss some common examples.

Typically, an extensible application either presents a user interface or provides a configuration system that enables a user to add a new add-in to the application. For example, new managed types, procedures, and so on are added to SQL Server by editing the SQL catalog, whereas some graphical applications include dialog boxes that enable users interactively to specify the add-ins they’d like to load. In other examples, add-ins are specified in code that the application interprets and runs. For example, add-ins are included in client-side Web pages using the <object> tag in a Hypertext Markup Language (HTML) source file.

Step 2: An Application Domain Is Chosen in Which to Load the New Add-In

In Chapter 5 I discuss the criteria to consider when partitioning a process into multiple application domains. These criteria include the need to isolate assemblies from others that are loaded in the same process, to unload code dynamically from a running process, and to limit the amount of communication that occurs between objects loaded in different application domains. When a new add-in is introduced to your extensible application, you must examine the add-in and load it into an application domain that meets your requirements for partitioning. Depending on your scenario, you might load the add-in into an existing application domain, or you might create a new one in which to load the add-in. For example, in Chapter 5 I describe how the Microsoft Internet Explorer host partitions a process into application domains based on Web sites. That is, all controls that are downloaded from the same site are loaded into the same application domain. As a result, when Internet Explorer comes across a reference to a control while parsing a Web page, it looks to see if it has already created an application domain corresponding to the site from which the control originates. If it has, the control is loaded into that domain. If not, a new application domain is created in which to load the control. Your application will likely follow similar logic when deciding how to load a new add-in. Most extensible applications keep an internal data structure that holds the list of application domains in the process along with some descriptive data for each domain that is used to determine the appropriate domain for new add-ins (in the Internet Explorer case, this extra piece of data is the name of a Web site).

Step 3: The Application Domain Manager in the Target Domain Is Called to Load the Add-In

After you’ve chosen an application domain in which to load the new add-in, you must transfer control into that target domain so the actual loading of the assembly can take place. As described, calling the assembly loading APIs from within the domain in which you’d like the add-in to run makes for a cleaner design. The easiest way to transition into a different application domain is to call a method on the application domain manager in the target domain. Look at some code from our boatracehost to see how this is done. Recall from Chapter 5 that the application domain manager for boatracehost is implemented in a class called BoatRaceDomainManager. BoatRaceDomainManager derives from an interface called IBoatRaceDomainManager, which includes a method called EnterBoat that we’ll use to load a new add-in into the application. Here’s a portion of BoatRaceDomainManager and the interface it derives from:

public interface IBoatRaceDomainManager
{
   // loads the boat identified by boatTypeName from the
   // assembly in assemblyName into the application domain
   // in which this instance of the domain manager is
   // running.
   void EnterBoat(string assemblyName, string boatTypeName);
}

public class BoatRaceDomainManager : AppDomainManager,
                                     IBoatRaceDomainManager
{
   void EnterBoat(string assemblyName, string boatTypeName)
   {
      // load the boat into this application domain...
   }
}

The following code uses the BoatRaceDomainManager class to load an assembly into a new application domain:

AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = @"c:Program FilesBoatRaceHostAddins";

AppDomain ad = AppDomain.CreateDomain("Alingi Domain",
                                    null,
                                    adSetup);

BoatRaceDomainManager adManager = (BoatRaceDomainManager)ad.DomainManager;

adManager.EnterBoat("AlingiBoat", "Alingi, Version=5.0.0.0,
                     PublicKeyToken=5cf360b40180107c,
                     culture=neutral);

In this example, we use the DomainManager property on System.AppDomain to get the instance of BoatRaceDomainManager that the CLR has created for us in the new domain. Given our domain manager instance, we simply call the EnterBoat method to transition into the new application domain.

Step 4: The Application Domain Manager in the Target Domain Loads the Add-In

Once inside the new application domain, using the assembly loading APIs to load the add-in is easy. Just as we used the AppDomain.Load method earlier in the chapter to load an assembly into a different application domain, you can use it now to load an assembly in the domain in which you’re running. The application domain manager from boatracehost does just this. The implementation of BoatRaceDomainManager.EnterBoat determines the current application domain using the static CurrentDomain property on System.AppDomain. It then calls the AppDomain.Load method, passing in the name of the assembly to load as shown in the following code:

public class BoatRaceDomainManager : AppDomainManager,
                                     IBoatRaceDomainManager
{
   void EnterBoat(string assemblyName, string boatTypeName)
   {
      // load the assembly containing boat into this
      // application domain...
      Assembly alingiAssembly = AppDomain.CurrentDomain.Load(assemblyName);

      // load the type from the new assembly...
   }
}

Using Assembly.Load and Related Methods

Now that I’ve shown how best to make use of the assembly loading APIs within your application, let’s dig into the details of the APIs themselves. As described, several methods in the .NET Framework provide the ability to load an assembly dynamically given an assembly identity—the AppDomain.Load method used in the previous section is just one such API. In some cases, multiple APIs provide the same functionality and are therefore redundant, but in other cases the APIs offer different capabilities. For example, some APIs enable you to load an assembly into a different application domain, whereas some load an assembly only into the current domain. Following are the methods in the .NET Framework that enable you to load an assembly and brief descriptions of each method’s capabilities. Keep in mind that these are the APIs that enable you to load an assembly given an assembly name. Several APIs enable you to load an assembly by providing the name of the file containing the manifest. I cover these APIs later on in the section, "Loading Assemblies by Filename."

  • AppDomain.Load. This is the only method that enables you to load an assembly into an application domain other than the one in which you’re currently running. As discussed earlier in this chapter, it’s easy to load an assembly into the current application domain inadvertently if you’re not careful.

    The overloads for AppDomain.Load are as follows:

    public Assembly Load (AssemblyName assemblyRef)
    
    public Assembly Load(AssemblyName assemblyRef, Evidence assemblySecurity)
    
    public Assembly Load(String assemblyString)
  • AppDomain.ExecuteAssemblyByName. This is the only method in the group that causes code to be executed when it is called. ExecuteAssemblyByName is used to launch managed executable files programmatically. You provide the pathname to the executable, and the CLR runs its main() method. This method doesn’t return until the executable has finished running.

    The overloads for AppDomain.ExecuteAssemblyByName are as follows:

    public int ExecuteAssemblyByName(String assemblyName)
    
    public int ExecuteAssemblyByName(String assemblyName,
       Evidence assemblySecurity)
    
    public int ExecuteAssemblyByName(String assemblyName,
       Evidence assemblySecurity,
       String[] args)
    
    public int ExecuteAssemblyByName(AssemblyName assemblyName,
       Evidence assemblySecurity,
       String[] args)
  • Assembly.Load. This is the most commonly used API for loading an assembly into the current application domain. Because it is static, there’s no way to use this method to load an assembly in an application domain other than the one in which you’re currently running.

    The overloads for Assembly.Load are as follows:

    public static Assembly Load(String assemblyString)
    
    public static Assembly Load(string assemblyString, Evidence assemblySecurity)
    
    static public Assembly Load(AssemblyName assemblyRef)
    
    static public Assembly Load(AssemblyName assemblyRef,
       Evidence assemblySecurity)
  • Assembly.LoadWithPartialName. This method has been deprecated in .NET Framework 2.0 and will be removed in a future version of the .NET Framework. Assembly.LoadWithPartialName enables you to load a strongly named assembly from the GAC using a partial reference. This was commonly used to implement a use latest version policy, whereby the caller would omit a version number from the reference and would load the latest version of the assembly from the GAC that matched the name and public key. Blindly loading the latest version of a shared assembly brings back the world of DLL Hell by exposing you to versioning conflicts between different releases of an assembly. For that reason, this method is being removed from the .NET Framework.

    The overloads for Assembly.LoadWithPartialName are as follows:

    static public Assembly LoadWithPartialName(String partialName)
    
    static public Assembly LoadWithPartialName(String partialName,
       Evidence securityEvidence)
    
    static public Assembly LoadWithPartialName(String partialName,
       Evidence securityEvidence, bool oldBehavior)

As you can see from the preceding list, the assembly loading APIs enable you to specify the assembly to load either by supplying its identity as a string or by providing an instance of System.Reflection.AssemblyName. In addition, each API has an overload that lets you associate security evidence with the assembly you are loading. I cover the details of using this parameter in Chapter 10.

Note

Note

It’s often the case that the first thing you’d like to do after loading an assembly is create a type from that assembly. To support this scenario, the .NET Framework provides several APIs that enable you to load an assembly and create a type with a single method call. When using these APIs, you pass the name of the type you’d like to create in addition to the name of the assembly you’d like to load. These convenience methods eliminate a lot of boilerplate code you’d find yourself writing over and over again. The methods that enable you to load an assembly and create a type are these:

  • System.AppDomain.CreateInstance

  • System.AppDomain.CreateInstanceAndUnwrap

  • System.Activator.CreateInstance

With respect to assembly loading, these methods work just like the ones in the preceding list, so I don’t talk about them explicitly in this chapter. Documentation of these methods can be found in the .NET Framework SDK.

Specifying Assembly Identities as Strings

When specifying an assembly identity by string, you must follow a well-defined format that the CLR understands. This format enables you to specify all four parts of an assembly’s name: the friendly name, version number, culture, and information about the public portion of the cryptographic key pair used to give the assembly a strong name. The string form of an assembly is as follows:

"<friendlyName>, Version=<version number>, PublicKeyToken=<publicKeyToken>,
   Culture=<culture>"

When specifying identities in this format, the <friendlyName> portion of the identity must come first. The PublicKeyToken, Version, and Culture elements can be specified in any order. Strings that follow this format are passed directly to the assembly loading APIs as shown in the following simple example:

public class BoatRaceDomainManager : AppDomainManager,
                                     IBoatRaceDomainManager
{
   void EnterAlingi()
   {
      // load the assembly into this application domain...
      Assembly a = Assembly.Load("Alingi, Version=5.0.0.1,
                                  PublicKeyToken=3026a3146c675483,
                                  Culture=neutral");
      // load the type from the new assembly...
    }
 }

As I explained earlier, it is possible to reference an assembly by supplying less than the full identity. I cover the details of how such references work in the section "Partially Specified Assembly References" later in the chapter.

Specifying assembly identities using the string format is generally straightforward as long as the CLR can correctly parse the string you supply. Any extra characters in the string (such as duplicate commas) or misspelled element names will cause a FileLoadException exception to be raised and your assembly will not be loaded.

Note

Note

The CLR error checking process when parsing assembly identities in .NET Framework 2.0 is much stricter than it was in previous versions of the CLR. For example, any extra characters or unrecognized element names (such as those caused by misspellings) were simply ignored instead of flagged as errors in previous versions. As always, make sure you thoroughly test your application on all versions of the CLR you intend to support to catch subtle differences like this.

Failures to load an assembly because of errors in parsing the assembly identity are easy to diagnose because the instance of FileLoadException that is thrown contains a specific message and HRESULT. The HRESULT indicating a parsing error is 0x80131047 (this error code is defined as FUSION_E_INVALID_NAME in the file corerror.h from the include directory in the .NET Framework SDK). The message property of the exception will say, "Unknown error -HRESULT 0x80131047."

In addition to forming the string correctly, it’s important that the values you supply for each element are valid. The following points summarize the valid values for friendly name, culture, and version.

  • Friendly name. Friendly names can contain any characters that are valid for naming files in the file system. Friendly names are not case sensitive.

  • Version. Assembly version numbers consist of four parts as specified in the following format:

    Major.minor.build.revision

    When specifying a version number, it’s best to include all four parts because the CLR will make sure that the version number of the loaded assembly exactly matches the version number you specify. You can omit values for some portions of the version number, but doing so results in a partial reference. When resolving an assembly based on a partial version number reference, the CLR matches only those portions of the version number you provide. This looseness in binding semantics can cause you to load an assembly inadvertently. For example, given the following reference:

    "Alingi, Version=5, PublicKeyToken=3026a3146c675483, Culture=neutral"

    the CLR only makes sure that the major number of the assembly you load is 5—none of the other portions of the version number are checked. In other words, the first assembly found in the application directory (the global assembly cache is not searched when resolving a partial reference) whose major number is 5 will be loaded regardless of the values for the other three portions of the version number. I cover partial references in more detail later in the chapter.

  • Culture. Values for the culture element of the assembly name follow a format described by RFC 1766. This format includes both a code for the language and a more specific code for the region. For example, "de-AT" is the culture value for German-Austria, whereas "de-CH" represents German-Switzerland. See the documentation for the System.Globalization.CultureInfo class in the .NET Framework SDK for more details.

The public key token portion of an assembly name requires a bit more explanation. Typing an entire 1024-bit (or larger) cryptographic key when referencing an assembly would be overly cumbersome. To make referencing strong-named assemblies easier, the CLR enables you to provide a shortened form of the key called a public key token. A public key token is an 8-byte value derived by taking a portion of a hash of the entire public key. Fortunately, the .NET Framework SDK provides a tool called the Strong Name utility (sn.exe) so you don’t have to be a cryptography wizard to obtain a public key token. The easiest way to obtain the public key token from an assembly is to use the -T option of sn.exe. For example, issuing the following command at a command prompt:

C:ProjectsAlingi> sn –T Alingi.dll

yields the following output:

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

Public key token is 3026a3146c675483

From here, you can paste the public key token value into your source code.

Specifying Assembly Identities Using System.Reflection.AssemblyName

Calling the assembly loading APIs by passing the assembly identity as a string is the most common approach because it’s so easy to use. However, as shown earlier, most of the assembly loading APIs also allow you to pass an instance of System.Reflection.AssemblyName to identify the assembly you want to load. AssemblyName has properties and methods that enable you to specify those elements of the assembly identity you wish to load in your assembly. Table 7-1 describes these members.

Table 7-1. Members of System.Reflection.AssemblyName Used to Load Assemblies

AssemblyName Member

Description

Name

A string used to specify the assembly’s friendly name

Version

An instance of System.Version that identifies the version of the assembly you’d like to load

CultureInfo

An instance of System.Globalization.CultureInfo that describes the assembly’s culture

SetPublicKey

SetPublicKeyToken

Methods that accept an array of System.Byte and that hold either the public key or the public key token of the assembly you wish to load

The following example shows how to call the assembly loading APIs by passing an instance of AssemblyName. In this example, I specify a partial reference using the friendly name only by constructing a new instance of AssemblyName, setting its Name property, and passing it to Assembly.Load:

public class BoatRaceDomainManager : AppDomainManager,
                                     IBoatRaceDomainManager
   {
       void EnterBoat()
       {
          // load the assembly into this
          // application domain...
          AssemblyName name = new AssemblyName();
          name.Name = "Alingi";

             Assembly a = Assembly.Load(name);

          // load the type from the new assembly
       }
   }

How the CLR Locates Assemblies

The CLR follows a consistent, well-defined set of steps to locate the assembly you’ve specified when calling one of the assembly-loading APIs. These steps are different based on whether you are referencing a strong-named assembly or a weakly named one.

Note

Note

All aspects of the CLR’s behavior for loading assemblies can be customized by you as the author of an extensible application. In Chapter 8, I write a CLR host that shows the extent of customization possible.

Understanding the steps the CLR follows to load an assembly is essential when building an extensible application that can work well in a variety of add-in deployment scenarios. Several factors influence both the version and the location of the assembly the CLR loads given your reference. Some factors are aspects of the deployment environment that you can control, such as the base directories for the application domains you create. Other factors, such as the specification of version policy by an administrator or the author of a shared assembly, are beyond your control. Fortunately, great tools are available for you to understand how the CLR locates assemblies and diagnose any problems you might encounter. I cover how to use these tools after describing the steps the CLR uses to locate assemblies.

The factors the CLR considers when resolving an assembly reference include deployment locations and the presence of any version policy or assembly codebase locations as shown in Figure 7-6 and described in the following points.

Factors that influence how the CLR locates assemblies

Figure 7-6. Factors that influence how the CLR locates assemblies

  • ApplicationBase. As described in Chapter 6, an application domain’s ApplicationBase establishes a root directory in which the CLR looks for assemblies intended to be private to that domain. You’ll almost always want to set this property when creating a new application domain.

  • Global assembly cache. The GAC is a repository for assemblies that are meant to be shared by several applications. The CLR looks in the GAC first when resolving a reference to a strong-named assembly, as I discuss in a bit.

  • Version policy. As described, version binding redirects can be specified either in an application configuration file, by the publisher of a strong-named assembly, or by the administrator of the machine. As the creator of an application domain, you can completely control whether application-level version redirects exist—you can turn off such version redirects either by not specifying a ConfgurationFile for your application domain or by setting the DisallowBindingRedirects property described in Chapter 6. However, there is no way for you to control whether binding redirects specified by the machine administrator are applied. It is possible that the CLR will load a version of an assembly other than the one you specify.

  • Codebases. The same configuration files used to specify version policy can also be used to provide a codebase location at which a given version of an assembly can be found. This is done using the <codebase> XML element (for more information on using <codebase> to supply an assembly location, see the .NET Framework SDK documentation). As with version redirects, you can control whether an application configuration file can be used to supply a codebase, but you can’t prevent a codebase location from being supplied by an administrator. Therefore, it is possible that the CLR will load a given assembly from a location other than what you expect. Later in the chapter, I show you how to determine where an assembly was loaded by using the properties and methods of the Assembly class.

How the CLR Locates Assemblies with Weak Names

Weakly named assemblies can be loaded only from an application domain’s ApplicationBase or a subdirectory thereof. As a result, the CLR’s rules for finding such an assembly are relatively straightforward. The CLR follows two steps when resolving a reference to an assembly with a simple name:

  1. Look for a codebase in the application configuration file.

  2. Probe for the assembly in the ApplicationBase and its subdirectories.

Step 1 rarely applies when loading add-ins into extensible applications. This is primarily because authoring a configuration file to specify a location for an assembly requires up-front knowledge that such an assembly will be loaded. As I mentioned, this isn’t the case with extensible applications because the add-ins are typically loaded dynamically. So the only way to use a configuration file to specify a codebase in this case is if somehow the configuration file was shipped along with the add-in and you set the ConfigurationFile property of your application to use it. Although possible, this scenario is unlikely to occur in practice.

Given that step 1 isn’t likely to apply, loading weakly named add-ins into extensible applications typically involves looking for the assembly in the ApplicationBase and its subdirectories. This process, termed probing, is described in detail in Chapter 6. Remember, too, that weakly named assemblies are loaded by name only—no other elements of the assembly name, such as the assembly’s version, are checked.

Note

Note

It is possible to end up loading a strongly named assembly given a reference that appears to be to a weakly named assembly. This happens if you have a strong-named assembly deployed somewhere in your ApplicationBase directory structure whose friendly name matches the name you are referencing using one of the assembly loading APIs. For example, given the following reference:

Assembly a = Assembly.Load("Alingi");

The CLR will load the first file it finds named alingi.dll, regardless of whether it has a strong name or a weak name. If the assembly it loads has a strong name, the CLR essentially starts over by taking the identity of the assembly and loading and treating that as a strong-name reference to resolve. In the next section I describe the steps involved in loading a strong-named assembly. A strong-named assembly could get loaded given the preceding reference because that reference is partial—no value is supplied for the public key token. This situation would not occur in scenarios in which the reference is fully specified, such as when an assembly is referenced in an early-bound fashion. If you want to be sure that only a weakly named assembly is loaded in this case, you must specify a null public key token like this:

Assembly a = Assembly.Load("Alingi, PublicKeyToken=null");

How the CLR Locates Assemblies with Strong Names

The process of loading a strongly named assembly is much more involved because of the potential for version policy and the existence of the GAC. The CLR takes the following steps to resolve a reference to a strong-named assembly:

  1. Determine which version of the assembly to load.

  2. Look for the assembly in the GAC.

  3. Look in the configuration files for any codebase locations.

  4. Probe for the assembly in the ApplicationBase and its subdirectories.

When loading a strongly named assembly, by default the CLR loads the version you specify in your reference. However, as described earlier in the chapter, that version can be redirected to another version of the same assembly by one of the three levels of version policy—application, publisher, or administrator. The first step the CLR takes in resolving a reference to a strongly named assembly is to compare the identity specified in the reference to the binding redirect statements in the three version policy files to determine whether an alternative version of the assembly should be loaded.

Next, the CLR looks for the assembly in the GAC. The CLR always prefers to load strongnamed assemblies from the GAC primarily for performance reasons. There are a few different reasons why loading from the GAC is better for overall system performance. First, if several applications are using the same strong-named assembly, loading the assembly from the same location on disk uses less memory than if each application were to load the same DLL from private locations. When a DLL is loaded from the same location multiple times, the operating system loads the DLL’s read-only pages only once and shares them among all instances. The second reason is related to how an assembly’s strong name is verified. Recall that a strong name involves a cryptographic signature. This signature must be verified to guarantee that the assembly hasn’t been altered since it was built. Verifying a cryptographic signature involves computing a hash of the entire contents of the file and other mathematically intense operations. As a result, it’s best to verify the signature at a time when its cost will be noticed the least (without compromising security, of course). An assembly’s strong name is verified during installation into the GAC. The cache is considered secure, so once the assembly has been successfully installed, its signature doesn’t have to be reverified. In contrast, because assemblies placed elsewhere in the file system (such as in an ApplicationBase directory) aren’t explicitly installed into a secure location, their strong-name signatures must be verified every time the assembly is loaded. By loading from the GAC, the CLR attempts to reduce the number of times these cryptographic signatures must be verified.

If an assembly cannot be found in the GAC, the CLR next looks to see whether a codebase location for the assembly has been provided in any of the configuration files. If such a location is found, the CLR uses it. If not, the CLR probes in the ApplicationBase directory just as it does for simply named assemblies.

Using System.Reflection.Assembly to Determine an Assembly’s Location on Disk

Once an assembly has been loaded, you can use the CodeBase, Location, and GlobalAssemblyCache properties of the Assembly class to determine information about where the CLR found it.

The Location and CodeBase properties are very similar in that they both provide information about the physical file from which the assembly was loaded. In fact, these two properties have the same value when an assembly is loaded from the local computer’s disk into an application domain that does not have shadow copy enabled. In this scenario, these two properties simply give you the name of the physical file on the local disk from which the assembly was loaded. The CodeBase and Location properties differ in two scenarios, however. First, if the assembly was downloaded from an HTTP server, the CodeBase property gives the location of the file on the remote server, whereas the Location property gives the location of the file in the downloaded files cache on the local machine. These properties also have different values if an assembly is loaded into an application domain in which shadow copy is enabled. In this scenario, CodeBase gives you the original location of the file, whereas Location tells you the location to which the file was shadow copied. See Chapter 6 for more information about how to enable shadow copy for the application domains you create.

Note

Note

The Assembly class also has a property called EscapedCodeBase that gives you the same pathname as CodeBase, except the value returned has the original escape characters.

The GlobalAssemblyCache property is a boolean value that tells you whether the CLR loaded the assembly from the GAC.

Using Fuslogvw.exe to Understand How Assemblies Are Located

The .NET Framework SDK includes a tool called the Assembly Binding Log Viewer (fuslogvw.exe) that is great not only for diagnosing errors encountered when loading assemblies, but also to help understand the assembly loading process in general. Fuslogvw.exe works by logging each step the CLR completes when resolving a reference to an assembly. These logs are written to .html files that can be viewed using the fuslogvw.exe user interface. The logging is turned off by default because of the expense involved in generating the log files. You can turn on logging in one of two modes: you can choose to log every attempt to load an assembly or log only those attempts that fail. Logging is enabled using the Settings dialog box from the fuslogvw.exe user interface as shown in Figure 7-7.

Enabling logging using fuslogvw.exe

Figure 7-7. Enabling logging using fuslogvw.exe

Take a look at how the output generated by fuslogvw.exe helps you understand how the CLR locates assemblies. After turning logging on, I ran boatracehost.exe and had it load an add-in from an assembly called TeamNZ. In this simple example, fuslogvw.exe logged that we attempted to load three assemblies as shown in Figure 7-8.

Fuslogvw.exe after running boatracehost.exe

Figure 7-8. Fuslogvw.exe after running boatracehost.exe

Double-clicking the row labeled TeamNZ displays the log generated while the CLR resolved the reference to that assembly. The log text is as follows:

0| *** Assembly Binder Log Entry (4/2/2004 @ 4:30:15 PM) ***
1| The operation was successful.
2| Bind result: hr = 0x0. The operation completed successfully.
3| Assembly manager loaded from:
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301mscorwks.dll
4| Running under executable
   C:Program FilesBoatRaceHostBoatRaceHostinDebugBoatRaceHost.exe
5| --- A detailed error log follows.
6| === Pre-bind state information ===
7| LOG: DisplayName = TeamNZ
 (Partial)
8| LOG: Appbase = file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/Debug/
9| LOG: Initial PrivatePath = NULL
10| LOG: Dynamic Base = NULL
11| LOG: Cache Base = NULL
12| LOG: AppName = BoatRaceHost.exe
13| Calling assembly : BoatRaceHost, Version=1.0.1553.29684, Culture=neutral,
   PublicKeyToken=null.
===
14| LOG: Attempting application configuration file download.
15| LOG: Download of application configuration file was attempted from
   file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/
   Debug/BoatRaceHost.exe.config.
16| LOG: Application configuration file does not exist.
17| LOG: Using machine configuration file from
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301configmachine.config.
18| LOG: Policy not being applied to reference at this time (private, custom,
   partial, or location-based assembly bind).
19| LOG: Attempting download of new URL
   file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/Debug/TeamNZ.dll.
20| LOG: Attempting download of new URL
   file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/
   Debug/TeamNZ/TeamNZ.dll.
21| LOG: Assembly download was successful. Attempting setup of file:
   C:Program FilesBoatRaceHostBoatRaceHostinDebugTeamNZTeamNZ.dll
22| LOG: Entering run-from-source setup phase. 23| LOG: A partially-
specified assembly bind succeeded from the application
   directory. Need to re-apply policy.
24| LOG: Policy not being applied to reference at this time (private, custom,
   partial, or location-based assembly bind).

I annotated the log text with line numbers so we can step through this in detail.

Lines 1–2 show whether the attempt to load the assembly succeeded. In error conditions, you can look up the HRESULT in the corerror.h file in the .NET Framework SDK to help determine what went wrong. However, the rest of the log explains the failure in detail.

Line 3 shows the directory from which the CLR was loaded. You can use this to determine which version of the CLR was running when this assembly bind was attempted.

Line 4 displays the name of the executable that initiated the assembly load. In our case, the executable is boatracehost.exe.

Line 7 shows the identity of the assembly we are trying to load. In late-bound cases such as this, this is the assembly identity that was passed to the assembly loading APIs. In addition to providing the identity, line 7 tells you whether the reference is partial or fully specified. This particular reference is partial. It was initiated with a simple call to Assembly.Load such as this:

Assembly a = Assembly.Load("TeamNZ");

Line 8 displays the ApplicationBase directory for the application domain in which the assembly load was initiated.

Lines 9–12 show some of the application domain properties that can affect how assemblies are loaded. These properties are covered in Chapter 6.

Line 13 gives the name of the assembly from which this assembly load was made. This information is useful for debugging in cases in which you might make the same attempt to load an assembly in several places throughout your application.

Lines 14–16 show the CLR attempting to find the configuration file associated with the application domain making the request. As described, this configuration file is consulted both for version policy information and for codebase locations.

Line 17 gives the location of the administrator configuration file. Again, this file can contain either version policy or codebase information.

Line 18 states that version policy is not being applied to this reference. In our case, version policy isn’t being applied because we have a partial reference. I discuss the output generated when resolving a fully qualified reference to a strong-named assembly in a bit. Version policy gets applied in that example.

Lines 19–22 show how the CLR probes for the assembly in the ApplicationBase directory. In this example, you can see that the first attempt to find the assembly failed, but the second one succeeded. The statement "Entering run-from-source setup phase" means that the CLR is loading the assembly directly from its location on disk. In contrast, if the assembly were located on an HTTP server, it would have to be downloaded first before it could be loaded.

Lines 23–24 state that the assembly was loaded from the ApplicationBase and that version policy is not being applied. In our case, version policy isn’t being applied because the assembly that was found has a weak name. If we had happened to load a strong-named assembly from the ApplicationBase, the CLR would look at the identity of the assembly that was loaded and go back and reapply version policy to determine whether a different version of the assembly should be loaded. If so, it would start the process of finding the assembly over again with the new reference.

You will see two primary differences in the log when you load a strong-named assembly—version policy is applied to the reference, and the CLR looks in the GAC as shown in the following output from fuslogvw.exe:

0| *** Assembly Binder Log Entry (4/4/2004 @ 12:27:26 PM) ***
1| The operation was successful.
2| Bind result: hr = 0x0. The operation completed successfully.
3| Assembly manager loaded from:
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301mscorwks.dll
4| Running under executable
   C:Program FilesBoatRaceHostBoatRaceHostinDebugBoatRaceHost.exe
5| --- A detailed error log follows.
6| === Pre-bind state information ===
7| LOG: DisplayName = Alingi, Version=5.0.0.0, Culture=neutral,
   PublicKeyToken=ae4cc5eda5032777
   (Fully specified)
8| LOG: Appbase = file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/Debug/
9| LOG: Initial PrivatePath = NULL
10| LOG: Dynamic Base = NULL
11| LOG: Cache Base = NULL
12| LOG: AppName = BoatRaceHost.exe
13| Calling assembly : BoatRaceHost, Version=1.0.1555.20566, Culture=neutral,
   PublicKeyToken=null.
===
14| LOG: Attempting application configuration file download.
15| LOG: Download of application configuration file was attempted from
   file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/
   Debug/BoatRaceHost.exe.config.
16| LOG: Application configuration file does not exist.
17| LOG: Using machine configuration file from
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301configmachine.config.
18| LOG: No redirect found in host configuration file.
19| LOG: Machine configuration policy file redirect found: 5.0.0.0 redirected
   to 6.0.0.0.
20| LOG: Post-policy reference: Alingi, Version=6.0.0.0, Culture=neutral,
   PublicKeyToken=ae4cc5eda5032777
21| LOG: Found assembly by looking in the GAC.

In this example, I used the .NET Framework Configuration tool to specify machine-level version policy to redirect the version of the assembly I’m referencing from 5.0.0.0 to 6.0.0.0. The differences between this assembly load and the previous one are shown in lines 7, 19, 20, and 21.

Line 7 shows that the reference is fully specified. Values are supplied for all four parts of the assembly’s name.

Line 19 shows that the CLR found my version policy statement in the machine configuration file.

Line 20 shows my reference after policy has been applied. Notice that the CLR is now looking for version 6.0.0.0 of Alingi.

Line 21 shows that the assembly was found in the GAC.

As you can see, stepping through the logs generated by fuslogvw.exe removes the mystery behind how the CLR locates assemblies. Fuslogvw.exe has several other options I haven’t discussed here. See the .NET Framework SDK documentation for more details.

Common Assembly Loading Exceptions

Failures to load assemblies typically show up in your application as one of three types of exceptions:

  • System.IO.FileNotFoundException. The FileNotFoundException is thrown when the assembly you specify in your reference cannot be found by the CLR.

  • System.IO.FileLoadException. As discussed earlier in this chapter, the FileLoadException is thrown when the CLR encounters an error while parsing the assembly name string you passed to one of the assembly loading APIs. This exception is also thrown when the CLR finds an assembly to load, but the assembly it finds doesn’t match all of the criteria specified in the reference. This scenario occurs most often when resolving partial references to strong-named assemblies located in the ApplicatonBase directory structure. For example, given the following reference:

    Assembly a = Assembly.Load(Alingi, PublicKeyToken=45d39a21bc3ff098 );

    the CLR will load the first file named alingi.dll it finds in the ApplicationBase directory structure. If the assembly it loads has a public key other than the one specified by the PublicKeyToken value in the reference, the CLR will throw a FileLoadException stating that the assembly it found doesn’t match the reference.

    Note

    Note

    The GAC is not searched in this case because the reference is partial. I explain more about how the CLR resolves partial references such as this later in the chapter (see "Partially Specified Assembly References").

  • System.BadImageFormatException. If the CLR finds a file to load, but the file is not a managed code assembly, a BadImageFormatException is thrown. This doesn’t happen often, but could occur if you have a native code file in your ApplicationBase directory structure with a filename matching that of an assembly you are referencing. More commonly, this exception occurs when loading an assembly by a filename as discussed later in the "Loading Assemblies by Filename" section.

All three exceptions have a string property called FusionLog that contains the text of a log file like those you viewed earlier in the discussion of fuslogvw.exe. In this way, you get the diagnostic information about why your call to the assembly loading APIs failed without having to enable logging using the fuslogvw.exe user interface.

Partially Specified Assembly References

As described, only the assembly’s friendly name is required when you’re using late-bound references. Values for the public key token, version, and culture can be omitted. Such partially specified assembly references are convenient to use, especially when your intent is to load weakly named assemblies, regardless of version, from your application directory. To do so, all you need to do is called Assembly.Load with the assembly’s friendly name as I’ve done several times throughout this chapter:

Assembly a = Assembly.Load(TeamNZ);

However, a few complexities might cause you to load an assembly unintentionally. As always, you can use the fuslogvw.exe tool to find out exactly what’s going on.

The following points summarize how the CLR treats a partially specified reference:

  • A partially specified reference always causes the ApplicationBase directory structure to be searched first. Searching never starts with the GAC. However, if a strong-named assembly is found in the ApplicationBase as a result of a partial reference, the CLR opens the file and extracts the strong-named assembly’s full identity. That identity then essentially is treated as a fully specified reference in that the CLR follows all the steps described earlier when looking for a strongly named assembly. Specifically, the CLR will evaluate version policy, look back in the GAC, and so on. If no policy is found, and the assembly is not found in the GAC, the file from the ApplicationBase is loaded. This is another example of how the CLR prefers to load an assembly from the GAC if possible.

  • If a public key token is specified in addition to the assembly’s friendly name, the value you specify is checked against any assemblies found in the ApplicationBase directory structure. If the keys don’t match, the CLR throws a FileLoadException stating that the identity of the assembly found didn’t match the reference.

  • If a version is specified in addition to the assembly’s friendly name, the behavior is different depending on whether the assembly found in the ApplicationBase has a strong name or a weak name. If the assembly has a strong name, the version number in the reference must match that of the assembly that is loaded. If not, a FileLoadException is thrown. If the assembly that is found has a weak name, the version number is not checked—the assembly is loaded regardless of version.

Loading Assemblies by Filename

At first glance, you might expect that loading an assembly by providing a filename would be much more straightforward than loading by assembly name. After all, instead of going through the steps to resolve the reference described earlier, the CLR could just directly load the file you supply. Unfortunately, things aren’t as simple as they seem. Although several APIs in the .NET Framework allow you to load an assembly by filename, none are guaranteed to load exactly the file you specify. There are two reasons for this. First, all strong-named assemblies loaded by filename are subject to version policy. This means that once the file is loaded, the CLR extracts its identity, looks to see whether any version policy applies to that identity, and if so, tries to load the new redirected version. The second reason you might get a different file than the one you specified is because the CLR has a set of binding rules it uses to force an application’s behavior to be deterministic regardless of the order in which its early-bound references are loaded. It’s not obvious how this requirement relates to loading assemblies dynamically by filename, so I describe this in detail.

You can use several APIs to load an assembly by filename dynamically, including the following:

  • System.Reflection.Assembly.LoadFrom

  • System.AppDomain.CreateInstanceFrom

  • System.AppDomain.CreateInstanceFromAndUnwrap

  • System.Activator.CreateInstanceFrom

  • System.Reflection.Assembly.LoadFile

These APIs can be grouped into two categories. Assembly.LoadFrom, AppDomain.CreateInstanceFrom(AndUnwrap), and Activator.CreateInstanceFrom all behave the same with respect to how assemblies are loaded. However, Assembly.LoadFile works differently. Historically speaking, Assembly.LoadFrom and its relatives were created first and shipped in the initial version of the .NET Framework (1.0). Assembly.LoadFile was introduced in .NET Framework 1.1 in an attempt to make loading by filename easier. However, the behavior of this API has now changed in .NET Framework 2.0, and its use is being discouraged. For that reason, this section focuses primarily on the Assembly.LoadFrom APIs.

Note

Note

From here on, all descriptions of Assembly.LoadFrom also apply to AppDomain.CreateInstanceFrom(AndUnwrap) and Activator.CreateInstanceFrom.

Subtleties of Assembly.LoadFrom

I mentioned earlier that the CLR makes sure applications behave deterministically regardless of the order in which their dependencies are loaded. To provide this guarantee, the CLR must ensure that assemblies loaded dynamically by filename do not conflict with the assemblies the application has referenced statically. Take a look at an example to better understand how these rules work and how they might affect you as the author of an extensible application.

The .NET Framework SDK contains a tool called regasm.exe. Regasm.exe takes an assembly as input and creates a set of registry keys that allow the public types in that assembly to be created from COM. What makes regasm.exe interesting for our example is not this core functionality, but rather the fact that it takes the filename of an assembly and loads it dynamically using Assembly.LoadFrom. You can expect to encounter this same sort of scenario in an extensible application—it’s entirely possible that your extensibility model involves obtaining the filenames of the add-in assemblies you’d like to load into your application domains.

Regasm.exe depends on a utility assembly called regcode.dll, which, for the purposes of this example, has a weak name and is installed in the same directory as regasm.exe.

C:
egasm
   Regasm.exe
   Regcode.dll

The dependency between these two assemblies is specified at compile time, so regasm.exe has an early-bound reference to regcode.dll recorded in its assembly manifest. Now say that a user invokes regasm.exe and passes in the filename to a completely different assembly that is coincidentally also called regcode.dll:

C:
egasm
egasm.exe c:	emp
egcode.dll

If the regcode.dll in c: emp were substituted for the "real" regcode.dll in the application directory, regasm.exe wouldn’t run if for no other reason than the types it expects to find in regcode.dll wouldn’t exist.

To solve this problem, the CLR isolates assemblies loaded using Assembly.LoadFrom from those that are referenced statically by the application by using a concept called binding contexts. Every application domain maintains two load contexts, or lists of loaded assemblies. One context, called the load context, contains those assemblies referenced statically by the application. The other context, the loadfrom context, contains those assemblies loaded dynamically given a filename. In our case, the regcode.dll from the ApplicationBase is loaded into the load context, and the regcode.dll that was loaded dynamically from c: emp is placed in the loadfrom context as shown in Figure 7-9. Notice also that the application also has some static dependencies on the .NET Framework assemblies; thus, they are loaded in the load context as well.

The CLR maintains a load context and a loadfrom context in every application domain.

Figure 7-9. The CLR maintains a load context and a loadfrom context in every application domain.

LoadFrom’s Second Bind

So far, I’ve said that all assemblies referenced statically by the application are placed in the load context and all assemblies loaded dynamically by filename are placed in the loadfrom context. There is one exception to this rule: if the assembly you are loading by filename would have been found were it referenced statically, that assembly is placed in the load context instead of the loadfrom context. For an assembly to be placed in the load context in this scenario, not only must it have the same identity as the assembly you are loading by filename, but it must be at the same location on disk. In other words, it must be exactly the same file.

This behavior is implemented by the CLR with what is known as LoadFrom’s second bind. It works like this: when you load a file using LoadFrom, the CLR opens the file, extracts its identity, and attempts to find an assembly that matches that identity through using the normal assembly resolution steps. If it finds a file, and the pathname matches the assembly loaded using LoadFrom, the CLR places that assembly in the load context instead of the loadfrom context. If the filenames are different, or if an assembly of that identity cannot be found, the assembly you loaded using LoadFrom is placed in the loadfrom context. You can see this behavior by looking at the output generated by fuslogvw.exe when LoadFrom is called. Let’s look at a specific example. Consider a scenario in which our boatracehost.exe loads add-in assemblies by filename using Assembly.LoadFrom. We can see both how the CLR treats filenamebased loads in general and the second bind specifically by looking at the following log that was generated after calling Assembly.Load with a filename of c: empalingi.dll:

0| *** Assembly Binder Log Entry (4/8/2004 @ 8:46:58 AM) ***
1| The operation was successful.
2| Bind result: hr = 0x0. The operation completed successfully.
3| Assembly manager loaded from:
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301mscorwks.dll
4| Running under executable
   C:Program FilesBoatRaceHostBoatRaceHostinDebugBoatRaceHost.exe
--- A detailed error log follows.

=== Pre-bind state information ===
5| LOG: Where-ref bind. Location = c:	empAlingi.dll
6| LOG: Appbase = file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/Debug/
7| LOG: Initial PrivatePath = NULL
8| LOG: Dynamic Base = NULL
9| LOG: Cache Base = NULL
10| LOG: AppName = BoatRaceHost.exe
11| Calling assembly : (Unknown).
===
12| WRN: Native image will not be probed in LoadFrom context. Native image will
   only be probed in default load context, like with Assembly.Load().
13| LOG: Attempting application configuration file download.
14| LOG: Download of application configuration file was attempted from
   file:///C:/Program Files/BoatRaceHost/BoatRaceHost/bin/
   Debug/BoatRaceHost.exe.config.
15| LOG: Application configuration file does not exist.
16| LOG: Using machine configuration file from
   C:WINDOWSMicrosoft.NETFrameworkv2.0.40301configmachine.config.
17| LOG: Attempting download of new URL file:///c:/temp/Alingi.dll.
18| LOG: Assembly download was successful. Attempting setup of file:
   c:	empAlingi.dll
19| LOG: Entering run-from-source setup phase.
20| LOG: Re-apply policy for where-ref bind.
21| LOG: No redirect found in host configuration file.
22| LOG: Post-policy reference: Alingi, Version=5.0.0.0, Culture=neutral,
   PublicKeyToken=ae4cc5eda5032777
23| LOG: GAC Lookup was unsuccessful.
24| LOG: Where-ref bind Codebase does not match what is found in default
   context.

The following lines show us what we’re looking for:

  • Line 5 indicates the assembly reference was made by filename, not by assembly name. The term where-ref comes from the fact that the bind was initiated by telling the CLR where the assembly is. You see this term from time to time throughout these logs.

  • Lines 17–19 show that the CLR succeeded in finding the file at c: empalingi.dll.

  • Lines 21–22 show the beginning of the second bind. In these lines, the CLR extracts the identity from the file just loaded and evaluates version policy. Because no policy was found, the identity of the assembly it looks for is that of the file just loaded. In this case, that’s Alingi, Version=5.0.0.0, Culture=neutral, PublicKeyToken=ae4cc5eda5032777.

  • Line 23 shows the CLR trying to find the assembly through its normal means. Because the assembly loaded by filename has a strong name, the CLR looks for it in the GAC.

  • Line 24 states that either the second bind didn’t find the assembly or, if it did, the assembly it found was at a different location than the one loaded by filename. As a result, the assembly at c: empalingi.dll is placed in the loadfrom context.

In practice, it’s not too likely that LoadFrom’s second bind will cause you trouble, although I’ve definitely seen people run into this. When it does happen, the result is usually confusion over type identity as I explain in the next section.

Binding Contexts and Type Identity

I’ve discussed how the CLR uses binding contexts to separate an application’s early-bound dependencies from those loaded dynamically by filename. However, to make this isolation complete, the CLR must also make sure that types of the same name from the different binding contexts are not mistaken for each other. The enforcement of this isolation effectively means that you cannot perform certain operations, such as casting, between types originating in different binding contexts. In some cases, this can lead to errors when you really expect that an operation involving two types should work. As an example, consider what would happen if regasm.exe attempted to cast an instance of a type in the loadfrom context to an instance in the load context as shown in the following code:

Assembly loadFromAssembly = Assembly.LoadFrom(@"c:	empRegcode.dll");
Object loadFromInstance =
    loadFromAssembly.CreateInstance("Regcode.UtilClass");
  Regcode.UtilClass loadInstance = (Regcode.UtilClass)loadFromInstance; //FAIL!

In this case, the type cast from the variable loadFromInstance to the variable loadInstance would fail. The variable loadFromInstance holds an instance of an object from the loadfrom context because the instance is created from an assembly loaded using LoadFrom. The variable loadInstance comes from the early-bound reference to regcode.dll because its declaration relies on the compiler being able to find the definition of Regcode.UtilClass at compile time.

If you see errors such as these in cases where you think it should work based on the source code, be suspicious of different type identities caused by assemblies in different binding contexts.

Loading Multiple Files with the Same Name

At this point, you should be getting a feel for the subtle complexities of the Assembly.LoadFrom API. You’ve seen cases in which the assembly you loaded can end up in the wrong binding context, causing errors in type operations, and how LoadFrom’s second bind can cause you to load an assembly with a completely different identity than the one you pointed to using a filename. In addition, in one more scenario you might end up loading an assembly other than the one you intend: if you load two assemblies with the same weak name from different locations, only one of them is loaded. This happens because the CLR allows only one assembly with a given weak name in the loadfrom context. Take a look at the following code, which loads two assemblies with the same weak name using LoadFrom:

Assembly alingiA = Assembly.LoadFrom(@ c:addinsAlingi.dll );
Assembly alingiB = Assembly.LoadFrom(
   @ c:program filesoatracehostcommonAlingi.dll");

When the first line is executed, the CLR loads the assembly at c:addinsalingi.dll into the loadfrom context. When executing the second line, the CLR looks in the loadfrom context and sees that an assembly with the simple name Alingi is already loaded. Instead of loading the assembly at c:program filesoatracehostcommonalingi.dll, the CLR simply returns the existing assembly. As a result, the variables alingiA and alingiB will both contain the assembly from c: empalingi—the assembly at c:program filesoatracehostcommonalingi will never be loaded.

The Loadfrom Context and Dependencies

Earlier in this chapter and in Chapter 6, I describe how the CLR looks for weakly named assemblies only within the ApplicationBase directory structure of the referencing application. As with many things, there is an exception to this rule. When you load an assembly with LoadFrom, the CLR adds the directory from which that assembly came to the list of directories in which it probes for static dependencies. For example, say that alingi.dll has an early-bound dependency on an assembly in spars.dll. Furthermore, boatracehost.exe loads alingi.dll from c: emp using Assembly.LoadFrom. In this case, you can deploy spars.dll to c: emp, and the CLR will find it, even though it is not located in boatracehost.exe’s ApplicationBase directory. This feature makes it convenient to deploy an add-in and all of its dependencies to the same directory. However, be aware that this directory is searched last. Specifically, the CLR looks in the GAC (if the assembly has a strong name) and in the ApplicationBase directory before consulting the directory from which the referring assembly was loaded. As a result, if the CLR happens to find an assembly that satisfies the reference to spars.dll (in this case) in any other location, that DLL would be loaded instead of the one in c: emp.

Note

Note

In an effort to reduce some of the confusion around using Assembly.LoadFrom, the CLR introduced a new API called Assembly.LoadFile in .NET Framework 1.1. The intent of this API was to load the exact file specified as opposed to issuing a second bind and doing identity checks that can cause an assembly other than the intended one to be loaded. Although LoadFile did work this way in .NET Framework 1.1, its behavior has been changed in .NET Framework 2.0 to be subject to version policy and rebinding just as LoadFrom is. As a result, the CLR team is discouraging its use. I expect LoadFile to be removed in a future version of the .NET Framework.

The ReflectionOnly APIs

The 2.0 version of the .NET Framework introduces a new set of APIs called the ReflectionOnly APIs. I mention them here only because the ReflectionOnly APIs provide a way to load an assembly by filename without any of the subtleties inherent in Assembly.LoadFrom. That is, you can use the ReflectionOnly APIs to load exactly the file you want—no policy is applied, no second bind occurs, and so on. Although this might sound like exactly what you’re looking for, the scenarios for which the ReflectionOnly APIs were built do not include the ability to load and execute assemblies dynamically. Specifically, the ReflectionOnly APIs enable you only to discover information about an assembly, they do not enable you to execute any code in that assembly. For this reason, they will not help you if you need to load and execute add-ins in an extensible application. For this reason, I don’t discuss them here. For more information, see the documentation for the following methods in the .NET Framework SDK guide:

  • Assembly.ReflectionOnly

  • Assembly.ReflectionOnlyLoad

  • Assembly.ReflectionOnlyLoadFrom

  • AppDomain.ReflectionOnlyGetAssemblies

  • AppDomain.ApplyPolicy

  • Type.ReflectionOnlyGetType

Loading Assemblies Using ICLRRuntimeHost

In addition to the managed assembly loading APIs discussed so far, the CLR provides a method that enables you to load an assembly and execute one of its methods using the unmanaged CLR hosting interfaces. This method, named ICLRRuntimeHost::ExecuteInDefaultAppDomain, is useful when you need to use the hosting interfaces to customize some aspect of the CLR, but don’t have a need to write any managed code as part of your host. These scenarios aren’t very common, but I could imagine needing to write a host that customizes how the CLR loads domain-neutral code using IHostControl or that enforces specific programming model constraints using ICLRHostProtectionManager, for example. In these scenarios, the customizations are available only through the CLR hosting interfaces. If these are the only customizations you need to make, and if your only other requirement is to be able to execute a managed method in the default application domain, ExecuteInDefaultAppDomain can satisfy your needs.

ExecuteInDefaultAppDomain loads an assembly given a filename. In addition to supplying the path to the assembly you want to load, you must supply the name of the method you want to execute and the name of the type that method is in. The method you supply must have a specific signature—it must be static, return an int, and have one string argument:

static int MethodName(string argument)

If you attempt to call a method with a signature other this, ExecuteInDefaultAppDomain returns with an HRESULT of 0x80131513 (COR_E_MISSINGMETHOD). The parameters to ExecuteInDefaultAppDomain are shown in Table 7-2.

Table 7-2. Parameters to ICLRRuntimeHost::ExecuteInDefaultAppDomain

Parameter

Description

pwzAssemblyPath

[in] The fully qualified path to the file containing the manifest of the assembly you’d like to load.

pwzTypeName

[in] The name of the type containing the method to execute. Remember to fully qualify the type name with the namespace the type is in.

pwzMethodName

[in] The name of the method to execute. Remember, this method must be static, return an int, and take a single string argument.

pwzArgument

[in] The argument to the method. The CLR imposes no format on this argument—it is completely up to you as the writer of the host.

pReturnValue

[out] The value returned from the method that was executed.

Example 7-1 shows a simple CLR host that uses ExecuteInDefaultAppDomain to execute a method in an assembly. In this example, I call CorBindToRuntimeEx to initialize the CLR and to get a pointer to the ICLRRuntimeHost interface. Given that pointer, I call ExecuteInDefaultAppDomain, passing in the path of the assembly to load along with the name of the method to execute.

Example 7-1. ExecApp.cpp

#include "stdafx.h"
#include <mscoree.h>

int main(int argc, wchar_t* argv[])
{
   ICLRRuntimeHost *pCLR = NULL;
   // initialize the CLR
   HRESULT hr = CorBindToRuntimeEx(
      L"v2.0.41013",
      L"wks",
      NULL,
      CLSID_CLRRuntimeHost,
      IID_ICLRRuntimeHost,
      (PVOID*) &pCLR);

   assert(SUCCEEDED(hr));

   // Any specific CLR customizations would be done here.

   // Start the CLR
   hr = pCLR->Start();
   assert(SUCCEEDED(hr));

   // Execute the application.
   DWORD retVal = 0;
   hr = pCLR->ExecuteInDefaultAppDomain(L"RealEstate.exe",
                                        L"RealEstate.Program",
                                        L"Start",
                                        NULL,
                                        &retVal);
    assert(SUCCEEDED(hr));

    return retVal; }

Capturing Assembly Load Events

The System.AppDomain class has an event called AssemblyLoad that is raised whenever an assembly is loaded into an application domain. AssemblyLoad provides notification when an assembly is loaded, but it doesn’t allow you to affect how the assembly is loaded in any way.

Note

Note

Several events do let you change how the assembly is loaded. These events, including AppDomain.AssemblyResolve, are discussed in detail in Chapter 8.

One scenario in which I find the AssemblyLoad event useful is in debugging. Earlier in the chapter I presented some examples that show how easy it is to load an assembly inadvertently into a different application domain than the one you intended. I’ve often used the AssemblyLoadEvent to trace all assemblies that get loaded into a process to help diagnose such problems.

To register for the AssemblyLoad event, you supply a delegate of type AssemblyLoadEventHandler. Instances of AssemblyLoadEventHandler have the sender and args parameters required by the .NET Framework event model as shown in the following declaration:

public delegate void AssemblyLoadEventHandler(Object sender,
                     AssemblyLoadEventArgs args);

The arguments passed to handlers of AssemblyLoad are of type AssemblyLoadEventArgs. The LoadedAssembly property of AssemblyLoadEventArgs identifies the assembly that has just been loaded.

The InitializeNewDomain method of your AppDomainManager class is a convenient place to register your event handler for the AssemblyLoad event. Recall from Chapter 6 that the CLR calls InitializeNewDomain from within each new application domain that is created. Placing your registration code here is a more foolproof way to make sure your handler is attached to all application domains than searching through your code looking for each call to AppDomain.CreateDomain is. The following example creates a new instance of AssemblyLoadEventHandler and registers it for the AssemblyLoad event within InitializeNewDomain. The event handler traces both the identity of the assembly and the friendly name of the application domain into which the assembly was loaded:

public class BoatRaceDomainManager : AppDomainManager, IBoatRaceDomainManager
{

// The event handler for AssemblyLoad
static void BoatRaceAssemblyLoadEventHandler(object sender,
 AssemblyLoadEventArgs args)
{    Trace.WriteLine("Assembly" + args.LoadedAssembly.FullName +
      " was loaded into" +AppDomain.CurrentDomain.FriendlyName);
}


public override void InitializeNewDomain(AppDomainSetupappDomainInfo)
{

   // Register a new instance of AssemblyLoadEventHandler to receive the
   // AssemblyLoad event.    AppDomain.CurrentDomain.AssemblyLoad += new
      AssemblyLoadEventHandler(BoatRaceAssemblyLoadEventHandler);
   }
}

Versioning Considerations for Extensible Applications

At the time of this writing, three major versions of the .NET Framework have been released: versions 1.0, 1.1, and 2.0. If you’re writing an extensible application that dynamically loads add-ins, it’s likely you’ll encounter an add-in built with a different version of the .NET Framework than your application is. If the add-in was built with an older version of the .NET Framework than your application was, it’s likely that everything will work fine because of the CLR’s commitment to backward compatibility. However, we all know that backward compatibility cannot be completely guaranteed. As a result, it’s useful to know how the CLR behaves in a process containing assemblies built with multiple versions of the .NET Framework. As with the other topics discussed in this chapter, dynamic, extensible applications are likely to encounter a greater range of versioning scenarios than applications in which all dependencies are known when the application is compiled. There are five main points to keep in mind when considering how add-ins built with various versions of the CLR will affect your extensible application:

  • In general, it’s not a good idea to try to load an add-in built with a newer version of the CLR than the version used to build your application. In fact, the CLR prevents you from loading an assembly built with .NET Framework 2.0 into a process that is running either .NET Framework 1.0 or .NET Framework 1.1. You can, however, load an assembly built with .NET Framework 1.1 into a process running .NET Framework 1.0, although it is not recommended.

  • Loading an add-in built with a version of the CLR older than the version used to build your application is generally OK. As described, the CLR’s commitment to backward compatibility means the add-in has a pretty good chance of working. If a particular addin doesn’t work in this scenario, it is sometimes possible to fix the problem by including version policy statements in your application’s configuration file. I describe this in more detail later in the chapter when I discuss overriding .NET Framework unification in the section "Overriding Unification."

  • As the author of the extensible application, you get to pick which version of the CLR is loaded into your process. The add-ins do not have a say in which version of the CLR is selected.

  • Once you’ve selected a version of the CLR to load into the process, the CLR automatically enforces that a matching set of .NET Framework assemblies comes with it. This concept, called .NET Framework unification, ensures that a consistent set of .NET Framework assemblies is loaded into the process. I talk about .NET Framework unification later in this chapter in the section "Microsoft .NET Framework Unification."

  • If .NET Framework unification introduces an assembly into your process that causes something to stop working, you can use your application configuration file to override the CLR’s choice of assembly version.

The rest of this section expands on these five points. Before I go on, however, look at a few .NET Framework APIs that are useful when dealing with versioning in extensible applications.

Determining Which Version of the CLR Was Used to Build an Assembly

As described in Chapter 4, every assembly contains information about the version of the .NET Framework it was compiled with. Being able to determine which version of the .NET Framework was used to build a particular add-in can be useful, especially if you begin to see problems in your application that you believe are version related. The version of the .NET Framework used to build an assembly can be obtained using the ImageRuntimeVersion property on System.Reflection.Assembly. Keep in mind, however, that the version number this property returns is the CLR version number, not the version number of the .NET Framework itself. For example, Assembly.ImageRuntimeVersion returns the string "v1.1.4322" for an assembly built with .NET Framework 1.1. Table 7-3 shows the CLR versions and the corresponding versions of the .NET Framework. Refer to Chapter 4 for a more complete description of how these version numbers relate.

Table 7-3. How CLR Version Numbers Map to .NET Framework Versions

CLR Version

.NET Framework Version

v1.0.3705

.NET Framework 1.0

v1.1.4322

.NET Framework 1.1

v2.0.41013

.NET Framework 2.0

There is one caveat when using the ImageRuntimeVersion property. The fact that ImageRuntimeVersion is a member of the Assembly class means that the CLR must load the assembly for which you’d like version information into the process before you can access the property. If you then decide that you don’t want to use the assembly, you can’t unload it without unloading the application domain containing the assembly. If you need to be able to determine which version of the CLR was used to build an assembly without having to load it, you need to use an unmanaged API. The CLR startup shim, mscoree.dll, provides an unmanaged API for exactly this purpose. This API, called GetFileVersion, takes an assembly’s filename and returns the version number used to build that assembly in a buffer. Here’s the signature for GetFileVersion from mscoree.idl in the .NET Framework SDK:

STDAPI GetFileVersion(LPCWSTR szFilename,
                      LPWSTR szBuffer,
                      DWORD  cchBuffer,
                      DWORD* dwLength)

The Extensible Application Chooses the Version

As discussed in Chapter 3, only one version of the CLR can be loaded into a given process. It’s up to you, as the author of the extensible application, to decide which version to load. The add-ins that you dynamically load into your process have no say in which version of the CLR is loaded. Furthermore, no infrastructure currently available allows an add-in to express a dependency on a particular version of the CLR.

As discussed in Chapter 3 and Chapter 4, it’s typically best to load the same version of the CLR that you used to build your application. Refer to those chapters for both the strategies to consider and the mechanics involved in loading the CLR.

At any point you can determine which version of the CLR is loaded into your process using the Version property on the System.Environment class. This property returns the CLR version, not the .NET Framework version. For example, the value of System.Environment.Version for a process running .NET Framework 2.0 is "2.0.41013." Table 7-3 contains the mapping between CLR version numbers and the corresponding versions of the .NET Framework.

Microsoft .NET Framework Unification

When you load an add-in assembly into your process, that assembly contains static references to the versions of the .NET Framework assemblies it was built against. For example, an assembly built with .NET Framework 1.1 has references to the 1.1 versions of System, System.XML, System.Data, and so on. If you load several add-ins into your process, some of which are built with different versions, you’ll have references to multiple versions of the .NET Framework assemblies. Figure 7-10 shows a scenario in which an extensible application built against .NET Framework 2.0 has loaded add-ins built against all three versions of the .NET Framework.

An extensible application with references to add-ins built with multiple versions of the .NET Framework

Figure 7-10. An extensible application with references to add-ins built with multiple versions of the .NET Framework

Given this scenario, the question arises as to whether it is preferable to load multiple versions of the .NET Framework assemblies into the same process or redirect the various references to a single version of the .NET Framework assemblies. Clearly, there are arguments for doing it either way. On the one hand, it might be desirable to load the exact version of the .NET Framework assemblies that a given add-in has requested. After all, presumably the add-in was tested against this version; therefore, it has the best chance to work. On the other hand, loading multiple versions of the .NET Framework assemblies into the same process has two complications: one is technical, whereas the other is a matter of logistics. Although it is technically feasible to load multiple versions of the same assembly into a given application domain or process, complications arise if two add-ins built against different versions of the .NET Framework need to communicate by exchanging types. Because the identity of the assembly in which a type is contained is part of that type’s identity, a given type from two different versions of the same assembly is considered a different type by the CLR. For example, an XMLDocument type from version 2.0.3600 of System.XML is a different type than the XMLDocument type from version 1.0.3705 of System.XML. As a result, the CLR throws an exception if an assembly tries to pass an instance of the 1.0.3705 version of XMLDocument to a method on a type expecting a 2.0.3600 version of XMLDocument. The amount different add-ins need to communicate with each other clearly varies by scenario, so it might be possible that this particular restriction isn’t an issue for you. However, the other complication that arises when multiple versions of the .NET Framework assemblies are loaded simultaneously is related to the consistency between assemblies. From the beginning, the .NET Framework assemblies were built to work as a matched set. Several interdependencies between these assemblies must remain consistent. Also, because mixing and matching these assemblies hasn’t been a priority yet, the amount of testing done by Microsoft to support these scenarios has been limited.

As a result of the complexities involved in loading multiple versions of the .NET Framework assemblies into a process or application domain, the default behavior of the CLR is to redirect all references to .NET Framework assemblies to the version of those assemblies that matches the CLR that is loaded into the process. The process of redirecting all references to this matched set is termed .NET Framework unification. The result of this unification is shown in Figure 7-11.

The CLR unifies all references to .NET Framework assemblies.

Figure 7-11. The CLR unifies all references to .NET Framework assemblies.

It’s important to remember that only references to the .NET Framework assemblies are unified. All other assembly references are resolved as is (subject to version policy, of course). For example, say you load two add-in assemblies that reference different versions of the same shared assembly called AcmeGridControl. Because AcmeGridControl is not a .NET Framework assembly, references to it will not be unified. Instead, both versions are loaded. The following is a list of those assemblies that are unified by the CLR:

  • mscorlib

  • System

  • System.Xml

  • System.Data

  • System.Data.OracleClient

  • System.Runtime.Remoting

  • System.Windows.Forms

  • System.Web

  • System.Drawing

  • System.Design

  • System.Runtime.Serialization.Formatters.Soap

  • System.Drawing.Design

  • System.EnterpriseServices

  • System.DirectoryServices

  • System.Management

  • System.Messaging

  • System.Security

  • System.ServiceProcess

  • System.Web.Mobile

  • System.Web.RegularExpressions

  • System.Web.Services

  • System.Configuration.Install

  • Accessibility

  • CustomMarshalers

  • cscompmgd

  • IEExecRemote

  • IEHost

  • IIEHost

  • ISymWrapper

  • Microsoft.JScript

  • Microsoft.VisualBasic

  • Microsoft.VisualBasic.Vsa

  • Microsoft.VisualC

  • Microsoft.Vsa

  • Microsoft.Vsa.Vb.CodeDOMProcessor

  • Microsoft_VsaVb

  • mscorcfg

  • vjswfchtml

  • vjswfccw

  • VJSWfcBrowserStubLib

  • vjswfc

  • vjslibcw

  • vjslib

  • vjscor

  • VJSharpCodeProvider

Overriding Unification

If the unification of .NET Framework assembly references causes problems in your scenario, you can override the unification using version policy statements. You shouldn’t have to resort to this too often because the CLR’s backward compatibility has been pretty good so far. However, it’s not inconceivable for you to encounter a compatibility issue that will cause you to want to load a different version of a .NET Framework assembly than the one the CLR selects by default. The best way to override unification is by issuing version policy statements in the configuration file for a particular application domain. This approach is preferable to using machine-wide policy because it affects only your application domain(s), not every process on the machine (also, it’s often the case that the machine configuration file is secured, so you can’t write to it anyway unless you’re an administrator).

To see how this works, consider an example in which a particular add-in that you must load has a strict dependency on the version of System.XML that shipped with version 1.1 of the .NET Framework, but you are running .NET Framework 2.0 in your process. To redirect the reference to System.XML from .NET Framework 2.0 back down to .NET Framework 1.1, you’d author a configuration file that looks like this:

<?xml version="1.0"?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Xml"
           publicKeyToken="b77a5c561934e089" />
        <bindingRedirect oldVersion="0.0.0.0-2.0.3600"
           newVersion="1.1.5000" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

This configuration file causes version 1.1.5000 of System.XML (the version that shipped in .NET Framework 1.1) to be loaded regardless of which version is referenced. Given this configuration file, you have the choice of how widely you’d like to apply this redirection. It can be that you want 1.1.5000 to be the only version of System.XML that is loaded in your process. In this case, you’d assign your configuration file to every application domain you create using the ConfigurationFile property of AppDomainSetup as described in Chapter 6. You might also choose to load System.XML version 1.1.5000 only into the application domain in which the add-in that requires it is running. If so, add-ins in other application domains that reference System.XML will get the unified version (the version that ships with .NET Framework 2.0).

Note that it is also possible to use version policy statements to cause all references to the .NET Framework assemblies to be redirected for a particular application domain. If you start by redirecting one reference, you might find inconsistencies that cause you to want to redirect the entire set of references to .NET Framework assemblies. In this way, you can cause two parallel stacks of .NET Framework assemblies to be loaded into the same process, yet be isolated from each other using application domain boundaries as shown in Figure 7-12.

Using a configuration file to override .NET Framework unification

Figure 7-12. Using a configuration file to override .NET Framework unification

Note

Note

Even though mscorlib is in the set of unified assemblies, you cannot use a bindingRedirect statement (or any other mechanism) to override the unification of mscorlib. The version of mscorlib to load is chosen by the CLR when the process starts and cannot be changed.

Summary

The dynamic nature of extensible applications makes loading assemblies more complicated than for applications in which all dependencies are statically referenced when the application is compiled. There are two reasons for this. First, the add-in assemblies that are added to the application must be loaded in a late-bound fashion. Loading assemblies on the fly like this requires the use of a set of methods in the .NET Framework called the assembly loading APIs. These APIs let you reference an assembly by providing either its name or the name of the file on disk that contains the assembly’s manifest. When using the assembly loading APIs, take care to make sure you are calling them from the application domain in which you’d like the assembly to be loaded. Otherwise, you often end up loading an assembly into an application domain that you didn’t intend to. The fuslogvw.exe utility from the .NET Framework SDK is a great tool not only to track down assembly loading failures, but also to understand how the process of loading assemblies works in general.

The other reason that loading assemblies in extensible applications can get complicated is the need to understand how assemblies built with different versions of the CLR interact in the same process. As the author of the extensible application, you get to decide which version of the CLR is loaded in your process. Given that, the CLR will automatically make sure that the versions of the .NET Framework assemblies that are loaded are the ones that were built and tested along with the CLR running in the process. If, for some reason, this default behavior doesn’t work in your scenario, you can always override it using a configuration file associated with your application domain.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.255.87