.Net Compact Framework: Versioning, Strong Names and the Global Assembly Cache – Part 2

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 https://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…

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.

Locating Assemblies

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).

Strong Named Reference

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.

Note: The ability to specify binding redirects in an application configuration file is new to the Whidbey version of the Compact Framework

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.

The full .Net Framework also allows codebase locations to be supplied in configuration files. If supplied, a codebase location is the only place the full .Net Framework CLR looks for the assembly. The .Net Compact Framework does not support codebase locations in configuration files.

Weak Named Reference

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.

Probing Rules

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.

As described in Part 1, the full .Net Framework has more elaborate probing rules than the Compact Framework does. In particular, the full .Net Framework also probes in subdirectories based on assembly name. For example, given a reference to an assembly named “utils” and an application base of “c:\MyApp”, the desktop will probe for:

   c:\MyApp\utils.dll

   c:\MyApp\utils\utils.dll

The Compact Framework does not probe in subdirectories based on assembly name.

The full .Net Framework only probes in the application base directory. In some ways, its unfortunate that .the Compact Framework probes in the root of the device because it violates the principle of “xcopy deployment” (the notion that an application can be simply replicated or deleted by manipulating a single directory). The fact that the Compact Framework probes the root of the device is a side affect of the fact that WinCE doesn’t have the concept of a current directory. Changing the probing rules now would be a breaking change and doing so would cause us to have to “coerce” it as described in my post on application compatibility (https://blogs.msdn.com/stevenpr/archive/2004/12/30/344540.aspx  ).

Satellite Assemblies

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:

c:\myapp\de\utils.dll

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.

Note: References to satellite assemblies are not recorded statically in metadata. Instead, all references to satellite assemblies are made in a late-bound fashion from classes such as System.Resources.ResourceManager.

Path-based References

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.

The full .Net Framework has the notion of load contexts. In this model, assemblies loaded via path-based references are often placed in a context called the LoadFrom context and isolated from those loaded via identity-based references. The .Net Compact Framework doesn’t have the formal notion of load contexts. Instead, we’ve implemented a few of the properties of the full .Net Framework’s LoadFrom context in order to simplify the deployment of assemblies loaded via path-based references. See Locating Dependencies later in this post.

Here are a few references that provide more information on the context model as implemented in the full .Net Framework:

https://www.gotdotnet.com/team/clr/LoadFromIsolation.aspx

https://blogs.msdn.com/suzcook/archive/2003/05/29/57143.aspx

Chapter 7 of Customizing the Common Language Runtime

The “Second Bind”

On the full .Net Framework, if the assembly referenced by file name has a strong name, the assembly identity is extracted from the file and an assembly bind is reissued. This is done primarily so that version policy is enforced for path-based references just as it is for references based on identity.

For example, say that Assembly.LoadFrom is called with a filename of “c:\temp\utils.dll”. The file at c:\temp\utils.dll has a strong name and a version number of 5.0.0.0. If there is a configuration file present that redirects version 5.0.0.0 of utils (+public key token) to version 6.0.0.0, the desktop will issue a bind for version 6.0.0.0. If found, version 6.0.0.0 will be loaded. If not found, the load will fail – version 5.0.0.0 will not be loaded.

The Compact Framework does not issue this second bind. We simply load the file specified in the path-based reference regardless of the presence of version policy.

Locating Dependencies

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:

Assembly.LoadFrom(”c:\temp\utils.dll”);

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

2. c:\myapp

3. c:\temp

Note that the probing of the “LoadFrom directory” for assemblies loaded via Assembly.LoadFrom is new to .NET CF 2.0. In 1.0, we only probed the application base (and device root) even when locating the dependencies referenced by assembly loaded via Assembly.LoadFrom. However, if an assembly was loaded via AppDomain.ExecuteAssembly, we did probe the directory from which the assembly was loaded for its dependencies.

Version Checking

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.

Strong Named Reference

Assembly version numbers in .NET consist of four parts:

<major>.<minor>.<build>.<revision>

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.

The full .Net Framework requires that all four portions of the version number to match, not just the first three. Allowing the revision number to float was originally implemented in .the Compact Framework to provide more flexibility in versioning given the lack of support for version policy in .NET CF 1.0. This behavior is maintained in Whidbey.

The 0.0.0.0 special case is unique to the Compact Framework – the full .Net Framework does not treat 0.0.0.0 differently than any other version number.

Weak Named Reference

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).

Allowing any version to satisfy a reference to a weakly named assembly is new to Whidbey. In 1.0, we forced the first three portions of the version number to match for weakly named references too.

Partial References

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.

On the full .Net Framework, if the value null is specified for public key token in the reference (as opposed to no value specified), the definition found through friendly name->file name matching must have a null public key token (this really just falls out from the fact that public key token values must match if specified). However, the Compact Framework treats a null public key token the same as if no value were specified. That is, we treat these references as though only the friendly name were specified and don’t require the definition we load to have a null public key token value

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.

Multi-file Assemblies

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.

Note: Support for multi-files assemblies is new to the Whidbey version of the Compact Framework.

 

Thanks,

Steven

This posting is provided "AS IS" with no warranties, and confers no rights.