.NET Framework: Building, Packaging, Deploying, and Administering Applications and TypesPart 2

Jeffrey Richter
This article assumes you're familiar with .NET and COM+
Level of Difficulty     1   2   3 
SUMMARY Part 1 of this series discussed how types built for the common language runtime can be shared among applications in the Microsoft .NET Framework regardless of the .NET languages used to build them. This second part continues with building assemblies by first covering security, sharing assemblies, versioning, localization, and side-by-side execution. Because in .NET two DLLs with the same name can be loaded as long as another attribute—which can include the localization language—differs, versioning is much easier than it used to be, so DLL Hell may become a thing of the past.
Last month, in part 1 of this article, I explained how Microsoft® .NET compilers produce metadata and Microsoft Intermediate Language (MSIL). I showed you how to examine this metadata using the ILDasm.exe tool that ships with the .NET Framework SDK. In addition, I explained what an assembly is, how to create private assemblies, and how an administrator can configure an application to help the common language runtime (CLR) locate these assemblies.
      This month I will explain how to create an assembly that can be used by multiple applications. You will learn how assembly versions are created and how the CLR's strict versioning policy comes into play.
      Please note that the functionality in this article is specific to Microsoft .NET Beta 1. Changes may occur with subsequent releases.

Shared Assemblies

      The runtime supports two different kinds of assemblies: private and shared. I covered private assemblies last month. In brief, private assemblies are deployed with an application and are for the sole use of that application. Private assemblies are typically authored by the same company that creates the application and, therefore, the company has a large degree of control over the naming, versioning, and behavior of the assembly. For this reason, the runtime imposes no administrative policies when binding to a private assembly.
      Shared assemblies are specifically designed for use by multiple applications. In fact, shared assemblies are typically created by one company and used by another company. For example, the Base Class Library that ships with the .NET Framework is a shared assembly, since all managed applications will use parts of it. At this point, versioning problems come into play. In this section, I'll discuss the creation of a shared assembly and the runtime policies that govern how an application binds to a shared assembly.
      For starters, you'll be happy to know that private assemblies and shared assemblies are structurally identical. That is, they use the same PE file format, metadata, and manifest tables that I discussed last month. And, you use the same tools, such as the C# compiler and AL.exe, to build shared assemblies. The real difference between private and shared assemblies has to do with how they are named, versioned, and where they are deployed on the user's drive. Also, the runtime enforces certain policies when an application tries to bind to a shared assembly.
      Private assemblies are identified by the name of the PE file (without extension) that contains the manifest. For private assemblies, this is sufficient since all assemblies that make up the application are usually packaged and deployed together. On the other hand, shared assemblies are created by different companies that are most likely not communicating with one another. This means that it's possible that two different companies could come up with the same name for their different assemblies. Obviously, installing these two different assemblies on the same machine is just asking for trouble.
      For shared assemblies to be useful, they must be placed in a location on the disk where the runtime can easily find them. This place is called the Global Assembly Cache (GAC) and can usually be found in the C:\WinNT\assembly directory. While developing and testing, you can install a shared assembly using a utility program that explicitly installs the assembly's files into the GAC. One such utility is AL.exe, which has a /I[nstall]:filename command-line option for just this purpose.
      You can also use the GacUtil.exe tool that ships with the .NET Framework SDK. Running this tool without any command-line arguments yields the usage information shown in Figure 1.
      If a shared assembly is packaged in a .cab file or is compressed in some way, then the assembly's file must first be decompressed to temporary files before installing the assembly into the GAC. Once the assembly has been installed, the temporary files may be deleted. Also, the Windows® Installer version 1.5 or later supports MSI files that want to install assemblies into the GAC. (You can determine which version of the Windows Installer is installed by running MSIExec.exe.) It is expected that MSI files will be the most common way for assemblies to get installed in the GAC.
      This installation of assembly files into the GAC is a form of registering the assembly, and breaks the goal of simple application installation, backup, moving, and uninstall. So, really, you only get the simple story when you use private assemblies exclusively.
      So, what is the purpose of registering an assembly in the GAC? Well, two different companies may each produce a Calculus assembly consisting of one file: Calculus.dll. Obviously, you can't put both of these files into the same shared directory because the last one installed would overwrite the first one, surely breaking some application. So, when you use a tool to install an assembly into the GAC, the tool creates a subdirectory under the C:\WinNT\Assembly directory and copies the assembly files into this subdirectory. The name of this subdirectory is generated algorithmically so that it is unique. For example, on my machine, I have a subdirectory called C96NCDM7. Normally, no one ever examines these directories, so the actual name doesn't matter.

Figure 2 The Assembly Directory
Figure 2 The Assembly Directory

      Explorer uses a shell extension when you view the assembly directory. For example, when I use the Explorer to navigate to my C:\WinNT\assembly directory, the view looks like Figure 2. In the figure, I've right-clicked on the System.WinForms assembly to show you the context menu. Of course, selecting Delete removes the assembly from the GAC. You can also delete an assembly with the GacUtil.exe tool.

Figure 3 System.WinForms Properties
Figure 3 System.WinForms Properties

      Selecting Properties from an assembly's context menu displays a property dialog box that looks like Figure 3. The Last Modified timestamp indicates when the assembly was added to the GAC. If I select the Platform tab, the dialog box looks like Figure 4.

Figure 4 Platform Properties
Figure 4 Platform Properties

      The GAC maintains a database file that consists of mapping information. Each entry contains an assembly's strong name and the subdirectory that contains its files. This brings up the question: "What is a strong name?" Every company that produces a shared assembly must have a way to uniquely identify that assembly. It was Microsoft's responsibility to devise some mechanism that allows this, and Microsoft chose to use standard public-key cryptographic techniques.

Strongly Named Assemblies

      To build a strong name assembly, you must first have a public/private key pair. This key pair is used to sign the assembly file.
      When you build a strongly named assembly, the assembly's FileDef manifest metadata table will include the list of all the files that make up the assembly. As each file's name is added to the manifest, the file's content is hashed and stored along with the file's name in the FileDef table. Note that you can override the default hash algorithm used in two ways: with AL.exe's /algid switch or with the assembly-level System.Reflection.AssemblyAlgIDAttribute custom attribute.
      After the PE file containing the manifest is built, the PE file's entire content is hashed, as shown in Figure 5. The hash algorithm used here is always SHA-1, and cannot be overridden. This hash value (typically around 100 or 200 bytes in size) is signed with the publisher's private key and the resulting RSA digital signature is stored in a reserved section (not included in the hash) within the PE file. The CLR header of the PE file is updated to reflect the location where the digital signature is embedded within the file.

Figure 5 Signing an Assembly
Figure 5 Signing an Assembly

      The publisher's public key is also embedded into the AssemblyDef manifest metadata table in this PE file as well. The combination of the file name and the public key gives this assembly a strong name, which is guaranteed to be unique. (There is no way that two companies could produce a Calculus assembly with the same public key, assuming the companies don't share this key pair with each other).
      At this point, the assembly and all of its files are ready to be distributed.
      Public keys are represented by a large number of bytes. In the case of the AssemblyRef metadata table, one public key must be associated with every referenced assembly. This means that a large percentage of the file's total size would be occupied by public key information.
      To conserve storage space, Microsoft hashes the public key and takes the last eight bytes of the hashed value. This reduced value has been determined to be statistically unique, and is therefore safe to pass around the system. These reduced public key values (also known as public key tokens) are stored in an AssemblyRef table and you can see them in the GAC's shell extension. By the way, the AssemblyDef manifest metadata table gets the full public key, not the public key token.
      When the assembly is registered with the GAC, a subdirectory is created, the files are copied into it, and the GAC's database gets a new entry. The new entry maps the name of the assembly, its public key, its version information, and its culture to the newly created subdirectory. I will discuss the version and culture properties momentarily. Note that the GAC can only accept strongly named assemblies; you cannot install private assemblies into the GAC. In addition, strongly named assemblies do not have to be placed in the GAC. An application can deploy a strongly named assembly in its application's directory and configure the application to use this assembly rather than an assembly that exists in the GAC.
      You should note that the GAC can hold multiple versions of a logical assembly. For example, the GAC can contain version 1.0.0.0 and version 2.0.0.0 of Calculus.dll. If an application was built and tested using version 1.0.0.0 of Calculus.dll, then the runtime will load version 1.0.0.0 of Calculus.dll for that application even though a later version of the assembly exists in the GAC.
      An administrator can change this version policy, but doing so is not recommended because the strict version policy resolves the classic DLL Hell problems. The application will behave as it always has because the code that it is executing is the same code with which it was built and tested. This also means that version 2.0.0.0 of Calculus.dll doesn't have to maintain backward compatibility with version 1.0.0.0. I'll talk more about version numbers in the next section.
      Signing a file with a private key ensures the identity of the file. When the assembly is installed in the GAC, the system hashes the PE file's contents and compares the hash value with the RSA digital signature value embedded within the PE file (after unsigning it with the public key). If the values are identical, then the file's contents have not been tampered and you know that you have the public key that corresponds with the publisher's private key. Note that the system only detects if the file containing the manifest has been altered at install time. Detecting whether one of the assembly's other files was tampered with is performed when the file is loaded at runtime. I'll talk more about public keys and hash values later in this article.
      With this mechanism, you cannot tell who the publisher is unless you know that the publisher produced the public key that you have. This also assumes that the publisher's key was never compromised. If the publisher wants to associate its identity with the assembly, then the publisher must use the Microsoft Authenticode® technology to ensure trust.
      When an application needs to bind to a shared assembly, the runtime passes the assembly properties (name, public key, version, and culture) to the GAC. The GAC examines its database for a match and, if a match is found, the subdirectory containing the desired assembly's files is returned. This also gives the caller the assurance that the assembly they load at runtime came from the same publisher that built the assembly they compiled against. If a match can't be made, then the bind fails and a TypeLoadException is thrown. Later in this article I'll discuss the details of assembly binding.

Delayed Signing

      Key pairs are generated by making calls into the CryptoAPI provided by Windows. These keys can be stored in files or other storage devices. For example, large organizations (like Microsoft) will maintain the returned private key in a hardware device that stays locked in a safe; only a few people in the company have any access to the private key. This prevents the private key from being compromised, ensuring the key's integrity. The public key is, well, public, and is freely distributed.
      When you're ready to package your strongly named assembly, you will have to use the secure private key to sign it. However, as you can imagine, getting access to the private key can be a huge burden when you're developing and testing the assembly. So, while developing an assembly, it would be nice to use a temporary private key, which would, of course, be less secure. This mechanism is called delayed signing.
      Delayed signing allows you to develop your assembly without using a private key at all. Basically, you get your company's public key value in a file and you pass the file name to whatever utility you use to build the assembly. If you're using AL.exe, then you'll use its /keyf[ile] switch to pass the file name. You must also use the /delay[sign] switch with AL.exe to inform it that you are not giving it a private key. At this point, the utility will embed the public key in the AssemblyDef table so that other assemblies that reference this assembly can use the public key token information properly. In addition, the utility will leave space in the resulting PE file for the RSA digital signature (the utility can determine how much space is necessary from the size of the public key). Note that the file's contents will not be hashed at this time either.
      Instead of using AL.exe's /keyf[ile] and /delay[sign] switches, you can embed the delay signing information directly in your source code using the following two assembly-level custom attributes:
    System.Reflection.AssemblyKeyFileAttribute
  System.Reflection.AssemblyDelaySignAttribute

The AssemblyKeyFileAttribute's constructor takes the name of the file containing the public key and the AssemblyDelaySignAttribute's constructor simply takes a Boolean value, indicating whether you want the assembly delay-signed or not.
      At this point, the resulting assembly does not have a valid signature. Normally, the runtime will assume that the assembly has been tampered with, and will fail to load it. To force the runtime to accept the assembly, you must tell it to skip verification of this assembly. This is accomplished using the SN.exe utility (with the -Vr switch) that ships with the .NET Framework SDK.
      When you're finished developing and testing the assembly, you'll need to officially sign it so that you can package and deploy it. To sign the assembly, use the SN.exe utility again, this time with the -R switch and the name of the file that contains the actual private key. This will cause SN.exe to hash the file's contents, sign it with the private key, and embed the RSA digital signature into the file where the space for it had previously been reserved. After this step, you may deploy the fully signed assembly.
      At the beginning of this section, I mentioned how organizations will keep their key pairs in a hardware device, like a smartcard. To keep these keys secure, the key values must never be persisted in a disk file. Cryptographic service providers (CSPs) offer containers that abstract the location of these keys. Microsoft, for example, uses a CSP that has a container that, when accessed, grabs the private key from a smartcard. If your private/public key pair is in a CSP container, you do not use AL.exe's /keyf[ile] switch or the AssemblyKeyFileAttribute custom attribute. Instead, you use AL.exe's /keyn[ame] switch and specify the name of the System.Reflection.AssemblyKeyNameAttribute assembly-level custom attribute.

Version Numbers

      Let's say that I've built, packaged, and deployed my application, which consists of multiple shared assemblies. A few months later, the Jeff Company makes a new version of the JeffTypes assembly. This new version adds several new features and fixes some bugs. I could send the JeffTypes assembly files to my customer and have them overwrite the old JeffTypes assembly files. Now, when the user runs the application, they get some new features and some bug fixes. Or do they?
      What if the updated JeffTypes assembly files accidentally introduce a new bug? Now the application doesn't work as it did before, and the user is very upset. In years past, this problem was much worse because many files were shared and, when one application updated the files, all applications started using the new files. There was no obvious relationship between the installation of a new application and an existing application that was no longer running properly.
      Today, it is highly recommended that file sharing be avoided if at all possible; use private assemblies whenever you can. You should also try to deploy strongly named assemblies privately whenever you can. Hard disk space is cheap and plentiful, so there is little gain by deleting multiple copies of the same file from the drive. Also, keeping files separated means that applications are isolated from one another; updating one application's files has no effect on another application. Of course, having similar (but different) assemblies loaded simultaneously increases the memory load on the system. But, in general, users will prefer "application working" over "reduced working set size."
      Ultimately, it comes down to this: once an application is deployed and running, it should stay running all the time. This means that you should never update any assembly files. However, sometimes bugs are reported and fixed, requiring that a new version of the assembly be deployed. Since you want the best of both worlds, the runtime must support some kind of assembly versioning policy.
      Note that the runtime only applies version policies to strongly named assemblies. For private assemblies, the runtime always uses whatever private assembly it can find, regardless of its version information.
      Every assembly has a version number associated with it. This version number consists of three logical parts and four physical parts, as shown in Figure 6. This figure shows an example of a version number: 2.5.719.2. The first two numbers make up the logical assembly version. In this example, I am building version 2.5 of the assembly. The third number, 719, indicates the build of the assembly. If your company builds its assembly every day, then you should increment the build number each day as well. The last number, 2, indicates the revision of the build. If for some reason your company has to build an assembly twice in one day, maybe to resolve a hot bug that is halting other work, then the revision number should be incremented.
      The runtime considers assemblies with different assembly versions to be different assemblies. That is, if an application is built requiring version 2.0.0.0 of an assembly, then the runtime will always bind that application to version 2.0.0.0 of that assembly. If the 2.0.0.0 version of the assembly files are deleted and version 2.5.0.0 of the assembly is available, then the runtime will fail the bind. This is the default versioning policy used by the runtime. Later, I will explain how an administrator can override this policy.
      However, if the runtime sees that there are two assemblies with the same major.minor version, the runtime will bind to the assembly with the latest build.revision. In other words, the runtime will bind to version 2.5.719.5 even if the application was bound to version 2.5.100.7 when it was built. Again, this is the default versioning policy employed by the runtime; an administrator can control this as well. (Note that the rules for how the CLR binds to an assembly are changing for Beta 2 of the .NET Framework.)
      To apply a version number to an assembly, you can either use the System.Reflection.AssemblyVersionAttribute assembly-level attribute, or you can use AL.exe's /version command-line switch. Either method embeds the specified version into the assembly's manifest. If you don't explicitly set a version number, most build tools will default to a version number of 0.0.0.0.
      For convenience, the AssemblyVersionAttribute and AL.exe's /version switch can generate default values for the minor, build, and revision numbers. The following lines demonstrate how you'd express this:
  [assembly:AssemblyVersion("1.*")]
[assembly:AssemblyVersion("1.5.*")]
[assembly:AssemblyVersion("1.5.2.*")]

When the CSC.exe or AL.exe see versions with asterisks in them, the tool automatically generates numbers for the missing parts. When generating missing values, the tool sets the minor part to 0, sets the build part to the number of days since January 1st, 2000, and sets the revision part to the number of seconds since midnight, local time, divided by two.
      Using the previous algorithms means that the build and revision numbers will always be unique and increase as the developer builds their assembly throughout the day. Also note that times are based on local times, not Greenwich Mean Time (GMT), so that the build number doesn't change in the middle of a day.
      When building a module, you must specify any referenced assemblies. As the metadata for the module is constructed, the compiler embeds in the AssemblyRef table the full version of the referenced assembly. When the module's MSIL code executes referring to an imported type, the runtime sees which assembly implements the type and uses the assembly name and version information to determine which assembly file to load.

Culture

      Assemblies are actually versioned using version numbers and culture information. For example, I could have an assembly that is strictly for German, another assembly for Swiss German, another assembly for U.S. English, and so on. Cultures are identified via a string that contains a primary and a secondary tag (as described in RFC 1766 on the IETF Web site at http://www.ietf.org/rfc/rfc1766.txt). Figure 7 provides some examples.
      You assign a culture string to an assembly using AL.exe's /c[ulture]:<text> switch or the System.Reflection.AssemblyCultureAttribute assembly-level attribute. For example:
  // Set assembly's culture to Swiss German
[assembly:AssemblyCulture("de-ch")]

If you don't explicitly assign a culture string, then the assembly is considered culture-neutral.
      When a strongly named assembly is added to the GAC, the assembly's culture information is saved in the GAC's database. When the runtime tries to bind to an assembly, the culture information is taken into account. In other words, the GAC could have two Calculus.dll assemblies in it: both assemblies have the same name, same version numbers, and the same public key, but different culture strings.
      When the runtime searches for the proper assembly to load, it will look for the exact assembly that the referencing assembly was built with. You can see that the AssemblyRef manifest table includes the culture information.
      It is highly recommended that you create one assembly that contains MSIL code and your application's default (or fallback) resource set. When building this assembly, don't specify a specific culture—don't use the /culture switch or the AssemblyCultureAttribute custom attribute. This is the assembly that other assemblies will reference in order to create and manipulate types.
      You can create one or more separate assemblies that contain only resources—no MSIL code at all. Assemblies that contain no MSIL code are called satellite assemblies. For these satellite assemblies, assign a culture that accurately reflects the culture of the resources placed in the assembly. You should create one satellite assembly for each culture you intend to support.

Side-by-side Execution

      In order to fully support shared assemblies, Windows has to be able to load multiple DLLs that have the same file name into a single process. Figure 8 shows an example where I have an assembly, App.exe, that references a strongly named assembly with a file name of Calculus.dll. App.exe also references another (private) assembly called AdvMath.dll, which itself references another strongly named assembly that happens to be in a file called Calculus.dll. However, this second Calculus.dll assembly has a different version and public key (the culture is the same, but it doesn't have to be).

Figure 8 Same File Names
Figure 8 Same File Names

      The CLR has the ability to load multiple files into a single address space even if the files have the same file name. This is called side-by-side execution, and it is a key component for solving the Windows DLL Hell problem. The ability to execute DLLs side-by-side is awesome because it allows you to create new versions of your assembly that do not have to maintain backward compatibility. Not having to maintain backward compatibility reduces all kinds of coding and testing time for a product and allows you to get that product to market faster.
      The developer must be aware of this side-by-side mechanism so that subtle bugs don't appear. For example, an assembly could create a named file-mapping kernel object and use the storage provided by this object. Another version of the assembly could also be loaded and attempt to create a file-mapping kernel object with the same name. This second assembly will not get new storage, but will instead access the same storage allocated by the first assembly. If not carefully coded, the two assemblies will stomp all over each other's data and the application will perform unpredictably.

Resolving Type References

      Last month's article contained the following source code:
    public class App {
   static public void Main(System.String[] args) {
      System.Console.WriteLine("Hi");
   }
}

This code is compiled and built into an assembly, say App.exe. To run this application, the App.exe PE file and the CLR PE file (MSCorEE.dll) must be loaded into the process. When the application runs, the runtime initializes and establishes an AppDomain for the application. The App.exe assembly is then loaded into the AppDomain, essentially just reading in the assembly manifest (note that the runtime does not do eager validation of the manifest—such as verifying that all of its files are present and/or that referenced assemblies are available).
      Next, the application's entry point, MethodDefToken, is obtained from the PE file's .NET header. MethodDefToken usually refers to a method called Main. From the MethodDef metadata table, the offset within the file for the method's MSIL code is located, just-in-time (JIT) compiled into native code (which includes having the code verified for type safety), and the native code starts executing. Here is the MSIL code for the Main method (obtained by calling ILDasm, of course):
  .method /*06000001*/ public hidebysig static 
        void  Main(class System.String[] args) il managed
{
  .entrypoint
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      "Hi"
  IL_0005:  call       void ['mscorlib'/* 23000001 */]System.Console
                       /* 01000003 */::WriteLine(class System.String)
  IL_000a:  ret
} // end of method 'App::Main'

      When the call to System.Console.WriteLine is reached, the runtime takes control in order to resolve the reference. To resolve the reference, the runtime sees that System.Console has a TypeRef ID of 0x01000003 and that this type is in an assembly that has an AssemblyRef ID of 0x23000001. If you refer back to the metadata dump, you'll see that these TypeRef and AssemblyRef IDs do in fact refer to the System.Console type defined in the MSCorLib assembly.
      When resolving a referenced type, there are three places where the type can be found:
  • Same file. Access to a type that is in the same file is early-bound, and the type is loaded out of the file directly and execution continues.
  • Different file, same assembly. The runtime ensures that the file being referenced is, in fact, in the assembly's FileRef table of the current assembly's manifest and then looks in the directory where the assembly's manifest file was loaded. The file is loaded, its hash value is checked to ensure the file's integrity, the type's member is found, and execution continues.
  • Different file, different assembly. This causes the runtime to load the necessary file that contains the referenced assembly's manifest. If this file doesn't contain the type, then the appropriate file is loaded. The type's member is found and execution continues.
      The ModuleDef, ModuleRef, and FileDef metadata tables refer to files using the file's name and its extension. However, the AssemblyRef metadata table refers to assemblies by file name without an extension. When binding to an assembly, the system appends a .dll file extension before attempting to locate the file.
      If any errors occur while resolving a type reference (file can't be found, file can't be loaded, hash mismatch, and so on), then a System.TypeLoadException exception is thrown.
      In the previous example, the runtime sees that System.Console is implemented in a different assembly than the caller. At this point, the runtime must search for the assembly and load the PE file that contains the assembly's manifest. Then, the manifest is scanned to determine which PE file implements the type. If it's the file that has the assembly manifest, then all is well. If it's in another file, then the runtime loads the other file and scans its metadata to locate the type's methods. Once the method is found, it is JIT-compiled and executed. Figure 9 shows how type binding occurs.
      When the runtime loads a new, strongly named assembly, it generates a public key token from the embedded public key. The generated public key token is then compared with the public key token embedded in the AssemblyRef entry. If the keys don't match, then a TypeLoadException exception is raised. This ensures that the referencing assembly was actually authored by the holder of the private key.
      When the runtime loads a new file, it hashes the newly loaded file and compares the resulting hash value with the hash value stored in the manifest whose FileRef table includes the file. If the values don't match, the newly loaded file's contents have been tampered with and cannot be trusted. Note that this check causes a runtime performance hit, since it is done whenever a file is loaded.

Advanced Administrative Control (Configuration)

      In the "Simple Administrative Control (Configuration)" section in last month's article, I gave a brief introduction of how an administrator can affect the way the runtime searches and binds to assemblies. In that section, I demonstrated how the administrator could move all of an assembly's files to another directory and add an XML configuration file that the runtime used to help locate the moved files.
      In this section, I'll take a more in-depth look at an administrator's configuration options. All configuration settings are maintained in the following XML files:
  • App.cfg (where App is the name of the application). This file controls application-specific settings and must be in the same directory as the application's main assembly files.
  • Admin.cfg. This file controls machine-specific settings and must be in the Windows directory (for example, C:\WinNT).
      Using these files, an administrator can exercise some control over the CLR's assembly binding rules. For example, let's say that App.exe references version 1.0.0.0 of Calculus.dll. An administrator can use the App.cfg file to indicate that any reference to version 1.0.0.0 of Calculus.dll should instead bind to version 2.0.0.0 of Calculus.dll. For the exact details of how the CLR binds to assemblies and how the information in these XML configuration files is used, please reference the "How the Runtime Locates Assemblies" section in the .NET Framework SDK documentation.
      These XML configuration files allow a publisher to update its assembly, swear that the new assembly is backward-compatible, and send it off to an administrator. The administrator can choose to believe the publisher, deploy the new assembly, and test the application. The important thing to note is that the system allows the use of an assembly that doesn't exactly match the assembly version recorded in the metadata. This extra flexibility really comes in handy.
      The exact format of the XML file used to configure these policies is described in the .NET Framework SDK documentation. I will not attempt to explain all the possibilities, but Figure 10 offers an example of a configuration file. The AppDomain tag tells the runtime to look for private assemblies in the application's directory first and then in the following subdirectories of the application directory: bin and AuxFiles (in that order). Again, please reference the "How the Runtime Locates Assemblies" section in the .NET Framework SDK help. It has a set of directory-probing rules that are more involved than what I describe here.
      The BindingMode tag tells the runtime to use normal version policy rules. That is, find the latest version of an assembly that matches the major.minor version that the application was built with. Instead of normal, safe could be specified, which would instruct the runtime to bind to the exact version of the assembly that the application was built with.
      The BindingRedir tag tells the runtime that any references to any version of the shared assembly, Calculus.dll (with originator 8e47bf1a5ed0ec84) should load version 3.3.10.0 of Calculus.dll instead. The UseLatestBuildRevision attribute indicates whether the latest build.revision of this assembly should be used or if the specified version must be used.
      The CodeBaseHint tag tells the runtime where it should look if an assembly's file can't be found on the user's machine. In this example, version 3.3.10.0 of Calculus.dll (with originator 8e47bf1a5ed0ec84) will be automatically downloaded from http://www.Wintellect.com/Calculus.dll.

Conclusion

      Assemblies and versioning try to solve some very difficult problems including:
  • How do you deploy an application and ensure that it never breaks?
  • When a bug is detected in a deployed app, how do you deploy an assembly that fixes the bug?
  • If you do deploy a bug-fixed version of the assembly, what do you do if it introduces a new bug?
  • If multiple apps use a shared assembly, how can you fix a bug experienced by one app (and presumably by multiple apps) without adversely affecting one of the other apps?
      It is not possible to build a platform that solves all of these problems because many of them are in direct conflict with one another. Historically, Windows hasn't even tried to solve these problems. Today, developers just build new versions of their DLL, do some testing in the hopes that no new bugs are introduced, and then the new DLL is deployed to a shared location in hopes that all apps using the shared DLL will not pick up the bug fix. In practice, this sounds nice and simple, but it frequently breaks down.
      In addition, it is impossible to define what a compatible component actually is. If a new version of a component is capable of throwing a new exception, is the component compatible with its older version? I'd say no. In fact, I'd assert that any change at all means that versions are not 100 percent compatible. While the assembly infrastructure is complex, it does offer a set of reasonable mechanisms to help solve the problems I just listed.
For related articles see:
How the Runtime Locates Assemblies
For background information see:
Assemblies and Security Considerations
The .NET Framework Class Library
Jeffrey Richter (http://www.wintellect.com) is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a cofounder of Wintellect (http://www.Wintellect.com), a software education, debugging, and consulting firm. He specializes in programming/design for .NET and Win32. Jeff is currently writing a Microsoft .NET Framework programming book and offers .NET technology seminars.

From the March 2001 issue of MSDN Magazine