Customizing the Microsoft .NET Framework Common Language Runtime

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

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 applicationit'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:\regasm \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:\regasm\regasm.exe c:\temp\regcode.dll

If the regcode.dll in c:\temp 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:\temp 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.

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:\temp\alingi.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:\WINDOWS\Microsoft.NET\Framework\v2.0.40301\mscorwks.dll 4| Running under executable C:\Program Files\BoatRaceHost\BoatRaceHost\bin\Debug\BoatRaceHost.exe --- A detailed error log follows. === Pre-bind state information === 5| LOG: Where-ref bind. Location = c:\temp\Alingi.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:\WINDOWS\Microsoft.NET\Framework\v2.0.40301\config\machine.config. 17| LOG: Attempting download of new URL file:///c:/temp/Alingi.dll. 18| LOG: Assembly download was successful. Attempting setup of file: c:\temp\Alingi.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 1719 show that the CLR succeeded in finding the file at c:\temp\alingi.dll.

  • Lines 2122 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:\temp\alingi.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:\temp\Regcode.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:\addins\Alingi.dll ); Assembly alingiB = Assembly.LoadFrom( @ c:\program files\boatracehost\common\Alingi.dll");

When the first line is executed, the CLR loads the assembly at c:\addins\alingi.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 files\boatracehost\common\alingi.dll, the CLR simply returns the existing assembly. As a result, the variables alingiA and alingiB will both contain the assembly from c:\temp\alingithe assembly at c:\program files\boatracehost\common\alingi 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:\temp using Assembly.LoadFrom. In this case, you can deploy spars.dll to c:\temp, 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:\temp.

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 wantno 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

    Категории