Last summer I posted an article that described the basic differences between the .Net Compact Framework and the full .Net Framework with respect to versioning and deployment (see http://blogs.msdn.com/stevenpr/archive/2004/06/30/170289.aspx ). Recently, I’ve come across a few other topics related to assembly loading in which our design differs from that of the full .Net Framework. This post extends Part 1 by providing more details on the topics I covered last summer, and by introducing the new differences I’ve only recently discovered. As a relative newcomer to the .Net Compact Framework team (Whidbey is my first major release) I found myself wanting to write a comprehensive document to sort out all the details of how the Compact Framework loads assemblies. This post is based on the research I’ve done to get more familiar with these features.
Throughout this post, the differences between the Compact Framework and the full .Net Framework are called out in shaded boxes. By the way, if you’re looking for a good reference on the details of how these features all work on the full .Net Framework, check out Suzanne Cook’s blog .
Before diving into the details, let’s establish some terminology…
The following definitions are used throughout this post:
Strong named assembly. An assembly that has been signed at compile time (or later using sn.exe). These assemblies have a non-null public key as part of their name, and include a signature generated with the corresponding private key. The signature is embedded within the file containing the assembly’s manifest.
Weakly named assembly. An assembly that has not been strong named signed as described above. These assemblies have a null public key as part of their name.
Assembly Reference. The data identifying the assembly an application wishes to load. An assembly reference consists of the desired assembly’s friendly name, and (optionally) the public key token, version and culture.
Fully-specified reference. An assembly reference in which the friendly name, public key token, version and culture are all specified.
Partial reference. An assembly reference in which at least one of the optional portions of the reference are not specified. That is, one of public key token, version or culture is missing.
Assembly Definition. An assembly’s identity as recorded in metadata.
Early bound reference. An assembly reference recorded in metadata at compile time. Early bound references are always fully specified.
Late bound reference. An assembly reference specified dynamically using an API such as Assembly.Load. Late bound references may be either fully or partially specified.
Identity based reference. An assembly reference made by specifying the requested assembly’s identity.
Path based reference. An assembly reference given by supplying the name of the file on disk that contains the requested assembly’s manifest.
Application base. The directory in which the main executable for the application is located. This directory forms the base directory for probing.
This section describes the specific steps the .Net Compact Framework CLR follows when resolving an assembly reference. In this section, all assembly references are assumed to be fully specified (partial references are covered later in this post).
If the assembly reference refers to a strong named assembly (that is, has a non-null value for public key token), the CLR follows these steps to resolve the reference:
1. Determine which version of the assembly to load. An application configuration file can be used to redirect an assembly reference to a different version. If a redirect is found that matches the reference, the version number in the redirect will be the version the CLR tries to load. If no redirect is found, the version found in the original reference is used.
2. Look for the assembly in the global assembly cache. If an assembly is found in the global assembly cache whose definition matches the reference, that assembly is loaded. If no assembly is found, the CLR proceeds to step 3. Matching the public key token, friendly name and culture is straightforward – an exact match is required. Matching the version number is more complicated. See Version Checking later in the post for the details.
3. Probe for the assembly. If no assembly is found in the global assembly cache, the CLR attempts to find a file on disk whose name (minus the extension) matches the friendly name of the assembly being referenced. This process, called probing, is described in Probing Rules below.
Searching for an assembly based on a weak reference (a reference with a null value for public key token) includes just one step:
1. Probe for the assembly. The global assembly cache is never searched when looking for weakly named assemblies.
The .Net Compact Framework CLR probes the following two directories (in order) when resolving an assembly reference:
1.The root directory on the device.
2.The application base directory.
In each location, the CLR attempts to find a file whose name (minus extension) matches the friendly name contained in the reference. We first look for matching files with a “.dll” extension, then with a “.exe” extension.
If a file is found, the definition for the assembly contained in that file is checked against the reference. The following rules apply:
· If the reference contains a null public key token, only the friendly name must match – version doesn’t matter.
· If the reference contains a non-null public key token, the version number and public key token must match as well as the friendly name. See Version Checking below on the rules for matching version numbers.
If the assembly found by probing fails to match the reference, an exception is thrown and probing is terminated – we do not continue to look for another match.
If the assembly reference contains a non-neutral value for culture, the CLR probes for the assembly in subdirectories based on the culture name instead of directly in the application base (or root). For example, given a reference with the friendly name “utils”, the culture “de” and an application base of “c:\myapp”, we’ll probe for:
If an assembly is not found that matches the culture specified in the reference, the CLR probes for additional “fallback” cultures as specified by a well-defined hierarchy of cultures. For example, if an assembly reference contains the culture “de-at”, the CLR will probe for “de” if an assembly whose culture is “de-at” cannot be found. The hierarchy of cultures used by the Compact Framework is the same as that used by the full .Net Framework.
Path-based references are specified by calls to Assembly.LoadFrom, AppDomain.ExecuteAssembly, or Assembly.Load (when called with the codebase property set in the AssemblyName).
The CLR takes the following steps to resolve a path-based reference:
1. The file specified in the reference is loaded and its friendly name extracted. If a weakly named assembly with that friendly name has already been loaded, that assembly is considered to satisfy the reference and the assembly identified by the path-based reference is discarded. Said differently, only one assembly of a given identity may be loaded at a time.
2. If no assembly with that friendly name is already loaded, the assembly identified by the path-based reference is loaded.
When an assembly is loaded given a path-based reference, the directory from which the assembly was loaded is added as an additional probing directory for that assembly’s dependencies. For example, given the following call:
From an application whose application base is c:\myapp, the CLR will probe the following directories to resolve assembly references made from utils (in order):
1. device root
This section describes the rules used by the Compact Framework to decide whether the version number in a given assembly definition matches that from a given assembly reference. The rules are different based on the reference contains a public key token or not. This section assumes that all assembly references are full. A discussion of partial references follows later in this post.
Assembly version numbers in .NET consist of four parts:
Assembly definitions and references contain all four parts of the version number.
The following rules apply when resolving a reference to a strongly named assembly:
1. The major, minor, and build portions of the version number from the reference must match in the definition in order for the reference to be satisfied.
2. If the reference contains the version number 0.0.0.0, the version number in the definition doesn’t matter – 0.0.0.0 is a special case that causes the version check to be skipped.
Version checking does not apply if the assembly reference contains a null value for public key token. That is, any definition with the same friendly name will be loaded (I’m ignoring culture in this statement).
Only the assembly’s friendly name is required when making a late-bound reference. Values for the public key token, version and culture may be omitted. For example, the following partial reference specifies only the desired assembly’s friendly name:
Assembly a = Assembly.Load(“TeamNZ”);
The following points summarize how the Compact Framework treats a partially-specified reference:
1. Partially specified references are never resolved from the global assembly cache – only the application base, root of the device, and (optionally) the “LoadFrom directory” are searched.
2. If only a friendly name is specified, we load the first file we find whose file name is the friendly name with a .dll extension. If we don’t find such a file, an exception is thrown.
3. If a public key token is specified in addition to the assembly name, we check to verify that the public key token in the reference matches the definition we find (the definition is found based on friendly name + .dll as in #2). If the public key tokens do not match, we fail – we do not continue looking.
4. If a version number is specified in addition to the assembly name, the behavior is different depending on whether the assembly found through friendly name->file name matching 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 (where “match” is described earlier in the section entitled Version Checking). If the version numbers don’t match, an exception is thrown and we stop searching. If the assembly that is found has a weak name, the version number is not checked – the assembly is loaded regardless of version.
One of the basic tenets of the original assembly design was that all files for a given assembly must be located in the same directory. So when looking for the non-manifest files in a multi file assembly, the only directory we look in is the directory in which the file containing the manifest lives.
When resolving a reference to a non-manifest file, we take the name given in the assembly manifest and look in the same directory (as manifest file) for a file with that name. If the multi-file assembly is strong named, the hash of the non-manifest file is computed when loaded and compared with the value stored in the manifest. If the hashes don’t compare, the load fails.
This posting is provided “AS IS” with no warranties, and confers no rights.