Simplifying Deployment and Solving DLL Hell with the .NET Framework

 

Steven Pratschner
Microsoft Corporation

Updated November 2001

Summary: This article introduces the concept of an assembly and describes how the .NET Framework uses assemblies to solve versioning and deployment problems. (16 printed pages)

Contents

Introduction
Problem Statement
Characteristics of the Solution
Assemblies: The Building Blocks
Versioning and Sharing
Version Policy
Deployment
Summary

Introduction

The Microsoft® .NET Framework introduces several new features aimed at simplifying application deployment and solving DLL Hell. Both end users and developers are familiar with the versioning and deployment issues that can arise with today's component-based systems. For example, virtually every end user has installed a new application on their machine, only to find that an existing application mysteriously stops working. Most developers have also spent time with Regedit, trying to keep all the necessary registry entries consistent in order to activate a COM class.

The design guidelines and implementation techniques used in the .NET Framework to solve DLL Hell are built on the work done in Microsoft Windows® 2000, as described by Rick Anderson in The End of DLL Hell, and by David D'Souza, BJ Whalen, and Peter Wilson in Implementing Side-by-Side Component Sharing in Applications (Expanded). The .NET Framework extends this previous work by providing features including application isolation and side-by-side components for applications built with managed code on the .NET platform. Also, note that Windows XP provides the same isolation and versioning features for unmanaged code, including COM classes and Win32 DLLs (see How To Build and Service Isolated Applications and Side-by-Side Assemblies for Windows XP for details).

This article introduces the concept of an assembly and describes how .NET uses assemblies to solve versioning and deployment problems. In particular, we'll discuss how assemblies are structured, how they are named, and how compilers and the Common Language Runtime (CLR) use assemblies to record and enforce version dependencies between pieces of an application. We'll also discuss how applications and administrators can customize versioning behavior through what we call version policies.

After assemblies are introduced and described, several deployment scenarios will be presented, providing a sampling of the various packaging and distribution options available in the .NET Framework.

Problem Statement

Versioning

From a customer perspective, the most common versioning problem is what we call DLL Hell. Simply stated, DLL Hell refers to the set of problems caused when multiple applications attempt to share a common component like a dynamic-link library (DLL) or a Component Object Model (COM) class. In the most typical case, one application will install a new version of the shared component that is not backward compatible with the version already on the machine. Although the application that has just been installed works fine, existing applications that depended on a previous version of the shared component might no longer work. In some cases, the cause of the problem is even more subtle. For example, consider the scenario where a user downloads a Microsoft ActiveX® control as a side effect of visiting some Web site. When the control is downloaded it will replace any existing versions of the control that were present on the machine. If an application that has been installed on the machine happens to use this control, it too might potentially stop working.

In many cases there is a significant delay before a user discovers that an application has stopped working. As a result, it is often difficult to remember when a change was made to the machine that could have affected the app. A user may remember installing something a week ago, but there is no obvious correlation between that installation and the behavior they are now seeing. To make matters worse, there are few diagnostic tools available today to help the user (or the support person who is helping them) determine what is wrong.

The reason for these issues is that version information about the different components of an application aren't recorded or enforced by the system. Also, changes made to the system on behalf of one application will typically affect all applications on the machine—building an application today that is completely isolated from changes is not easy.

One reason why it's hard to build an isolated application is that the current run-time environment typically allows the installation of only a single version of a component or an application. This restriction means that component authors must write their code in a way that remains backward compatible, otherwise they risk breaking existing applications when they install a new component. In practice, writing code that is forever backward compatible is extremely difficult, if not impossible. In .NET, the notion of side by side is core to the versioning story. Side by side is the ability to install and run multiple versions of the same component on the machine at the same time. With components that support side-by-side, authors aren't necessarily tied to maintaining strict backward compatibility because different applications are free to use different versions of a shared component.

Deployment and Installation

Installing an application today is a multi-step process. Typically, installing an application involves copying a number of software components to the disk and making a series of registry entries that describe those components to the system.

The separation between the entries in the registry and the files on disk makes it very difficult to replicate applications and to uninstall them. Also, the relationship between the various entries required to fully describe a COM class in the registry is very loose. These entries often include entries for coclasses, interfaces, typelibs, and DCOM app IDs, not to mention any entries made to register document extensions or component categories. Oftentimes you end up keeping these in sync manually.

Finally, this registry footprint is required to activate any COM class. This drastically complicates the process of deploying distributed applications because each client machine must be touched to make the appropriate registry entries.

These problems are primarily caused by the description of a component being kept separate from the component itself. In other words, applications are neither self-describing nor self-contained.

Characteristics of the Solution

The .NET Framework must provide the following basic capabilities to solve the problems just described:

  • Applications must be self-describing. Applications that are self-describing remove the dependency on the registry, enabling zero-impact installation and simplifying uninstall and replication.
  • Version information must be recorded and enforced. Versioning support must be built into the platform to ensure that the proper version of a dependency gets loaded at run time.
  • Must remember "last known good." When an application successfully runs, the platform must remember the set of components—including their versions—that worked together. In addition, tools must be provided that allow administrators to easily revert applications to this "last known good" state.
  • Support for side-by-side components. Allowing multiple versions of a component to be installed and running on the machine simultaneously allows callers to specify which version they'd like to load instead of a version "forced" on unknowingly. The .NET Framework takes side by side a step farther by allowing multiple versions of the framework itself to coexist on a single machine. This dramatically simplifies the upgrade story, because an administrator can choose to run different applications on different versions of the .NET Framework if required.
  • Application isolation. The .NET Framework must make it easy, and in fact the default, to write applications that cannot be affected by changes made to the machine on behalf of other applications.

Assemblies: The Building Blocks

Assemblies are the building blocks used by the .NET Framework to solve the versioning and deployment issues just described. Assemblies are the deployment unit for types and resources. In many ways an assembly equates to a DLL in today's world; in essence, assemblies are a "logical DLLs."

Assemblies are self-describing through metadata called a manifest. Just as .NET uses metadata to describe types, it also uses metadata to describe the assemblies that contain the types.

Assemblies are about much more than deployment. For example, versioning in .NET is done at the assembly level—nothing smaller, like a module or a type, is versioned. Also, assemblies are used to share code between applications. The assembly that a type is contained in is part of the identity of the type.

The code access security system uses assemblies at the core of its permissions model. The author of an assembly records in the manifest the set of permissions required to run the code, and the administrator grants permissions to code based on the assembly in which the code is contained.

Finally, assemblies are also core to the type system and the run-time system in that they establish a visibility boundary for types and serve as a run-time scope for resolving references to types.

Assembly Manifests

Specifically, a manifest includes the following data about the assembly:

  • Identity. An assembly's identity consists of four parts: a simple text name, a version number, an optional culture, and an optional public key if the assembly was built for sharing (see section on Shared Assemblies below).
  • File list. A manifest includes a list of all files that make up the assembly. For each file, the manifest records its name and a cryptographic hash of its contents at the time the manifest was built. This hash is verified at run time to ensure that the deployment unit is consistent.
  • Referenced assemblies. Dependencies between assemblies are stored in the calling assembly's manifest. The dependency information includes a version number, which is used at run time to ensure that the correct version of the dependency is loaded.
  • Exported types and resources. The visibility options available to types and resources include "visible only within my assembly" and "visible to callers outside my assembly."
  • Permission requests. The permission requests for an assembly are grouped into three sets: 1) those required for the assembly to run, 2) those that are desired but the assembly will still have some functionality even if they aren't granted, and 3) those that the author never wants the assembly to be granted.

The IL Disassembler (Ildasm) SDK tool is useful for looking at the code and metadata in an assembly. Figure 1 is an example manifest as displayed by Ildasm. The .assembly directive identifies the assembly and the .assembly extern directives contain the information about other assemblies on which this one depends.

Figure 1. Example manifest as displayed by the IL Disassembler

Assembly Structure

So far, assemblies have been described primarily as a logical concept. This section helps make assemblies more concrete by describing how they are represented physically.

In general, assemblies consist of four elements: the assembly metadata (manifest), metadata describing the types, the intermediate language (IL) code that implements the types, and a set of resources. Not all of these are present in each assembly. Only the manifest is strictly required, but either types or resources are needed to give the assembly any meaningful functionality.

There are several options for how these four elements can be "packaged." For example, Figure 2 shows a single DLL that contains the entire assembly: the manifest, the type metadata, IL code, and resources.

Figure 2. DLL containing all assembly elements

Alternatively, the contents of an assembly may be spread across multiple files. In Figure 3, the author has chosen to separate some utility code into a different DLL and to keep a large resource file (in this case a JPEG) in its original file. One reason this might be done is to optimize code download. The .NET Framework will download a file only when it is referenced, so if the assembly contains code or resources that are accessed infrequently, breaking them out into individual files will increase download efficiency. Another common scenario in which multiple files are used is to build an assembly that consists of code from more than one language. In this case, you’d build each file (module) separately, then group them into an assembly using the Assembly Linker tool provided in the .NET Framework SDK (al.exe).

Figure 3. Assembly elements spread across multiple files

Versioning and Sharing

One of the primary causes of DLL Hell is the sharing model currently used in component-based systems. By default, individual software components are shared by multiple applications on the machine. For example, every time an installation program copies a DLL to the system directory or registers a class in the COM registry, that code will potentially have an effect on other applications running on the machine. In particular, if an existing application used a previous version of that shared component, that application will automatically start using the new version. If the shared component is strictly backward compatible this may be okay, but in many cases maintaining backward compatibility is difficult, if not impossible. If backward compatibility is not maintained, or cannot be maintained, this often results in applications that are broken as a side effect of other applications being installed.

A principle design guideline in .NET is that of isolated components (or assemblies). Isolating an assembly means that an assembly can only be accessed by one application—it is not shared by multiple applications on the machine and cannot be affected by changes made to the system by other applications. Isolation gives a developer absolute control over the code that is used by his application. Isolated, or application-private, assemblies are the default in .NET applications. The trend toward isolated components started in Microsoft Windows 2000 with the introduction of the .local file. This file was used to cause both the OS Loader and COM to look in the application directory first when trying to locate the requested component. (See the related article in the MSDN Library, Implementing Side-by-Side Component Sharing in Applications.)

However, there are cases where sharing an assembly between applications is necessary. It clearly wouldn't make sense for every application to carry its own copy of System.Windowns.Forms, System.Web or a common Web Forms control.

In .NET, sharing code between applications is an explicit decision. Assemblies that are shared have some additional requirements. Specifically, shared assemblies should support side by side so multiple versions of the same assembly can be installed and run on the same machine, or even within the same process, at the same time. In addition, shared assemblies have stricter naming requirements. For example, an assembly that is shared must have a name that is globally unique.

The need for both isolation and sharing leads us to think of two "kinds" of assemblies. This is a rather loose categorization in that there are no real structural differences between the two, but rather the difference is in how they will be used: whether private to one application or shared among many.

Application-Private Assemblies

An application-private assembly is an assembly that is only visible to one application. We expect this to be the most common case in .NET. The naming requirements for private assemblies are simple: The assembly names must only be unique within the application. There is no need for a globally unique name. Keeping the names unique isn't a problem because the application developer has complete control over which assemblies are isolated to the application.

Application-private assemblies are deployed within the directory structure of the application in which they are used. Private assemblies can be placed directly in the application directory, or in a subdirectory thereof. The CLR finds these assemblies through a process called probing. Probing is simply a mapping of the assembly name to the name of the file that contains the manifest.

Specifically, the CLR takes the name of the assembly recorded in the assembly reference, appends ".dll" and looks for that file in the application directory. There are a few variants on this scheme where the Runtime will look in subdirectories named by the assembly or in subdirectories named by the culture of the assembly. For example, a developer may choose to deploy the assembly containing resources localized to German in a subdirectory called "de" and to Spanish in a directory called "es." (See the .NET Framework SDK Guide for details.)

As just described, each assembly manifest includes version information about its dependencies. This version information is not enforced for private assemblies because the developer has complete control over the assemblies that are deployed to the application directory.

Shared Assemblies

The .NET Framework also supports the concept of a shared assembly. A shared assembly is one that is used by multiple applications on the machine. With .NET, sharing code between applications is an explicit decision. Shared assemblies have some additional requirements aimed at avoiding the sharing problems we experience today. In addition to the support for side by side describe earlier, shared assemblies have much more stringent naming requirements. For example, a shared assembly must have a name that is globally unique. Also, the system must provide for "protection of the name"—that is, preventing someone from reusing another's assembly name. For example, say you're a vendor of a grid control and you've released version 1 of your assembly. As an author you need assurance that no one else can release an assembly claiming to be version 2 or your grid control. The .NET Framework supports these naming requirements through a technique called strong names (described in detail in the next section).

Typically, an application author does not have the same degree of control over the shared assemblies used by the application. As a result, version information is checked on every reference to a shared assembly. In addition, the .NET Framework allows applications and administrators to override the version of an assembly that is used by the application by specifying version policies.

Shared assemblies are not necessarily deployed privately to one application, although that approach is still viable, especially if xcopy deployment is a requirement. In addition to a private application directory, a shared assembly may also be deployed to the Global Assembly Cache or to any URL as long as a codebase describing the location of the assembly is supplied in the application’s configuration file. The global assembly cache is a machine-wide store for assemblies that are used by more than one application. As described, deploying to the cache is not a requirement, but there are some advantages to doing so. For example, side-by-side storage of multiple versions of an assembly is provided automatically. Also, administrators can use the store to deploy bug fixes or security patches that they want every application on the machine to use. Finally, there are a few performance improvements associated with deploying to the global assembly cache. The first involves the verification of strong name signatures as described in the Strong Name section below. The second performance improvement involves working set. If several applications are using the same assembly simultaneously, loading that assembly from the same location on disk leverages the code sharing behavior provided by the OS. In contrast, loading the same assembly from multiple different locations (application directories) will result in many copies of the same code being loaded. Adding an assembly to the cache on an end user's machine is typically accomplished using a setup program based on the Windows Installer or some other install technology. Assemblies never end up in the cache as a side effect of running some application or browsing to a Web page. Instead, installing an assembly to the cache requires an explicit action on the part of the user. Windows Installer 2.0, which ships with Windows XP and Visual Studio .NET, has been enhanced to fully understand the concept of assemblies, the assembly cache and isolated applications. This means you will be able to use all of the Windows Installer features, such as on-demand install and application repair, with your .NET applications.

It’s often not practical to build an install package every time you want to add an assembly to the cache on development and test machines. As a result, the .NET SDK includes some tools for working with the assembly cache. The first is a tool called gacutil that allows you to add assemblies to the cache and remove them later. Use the /i switch to add an assembly to the cache:

gacutil /i:myassembly.dll 
See the .NET Framework SDK documentation for a full description of the 
      options supported by gacutil.

The other tools are a Windows Shell Extension that allows you to manipulate the cache using the Windows Explorer and the .NET Framework Configuration Tool. The Shell Extension can be accessed by navigating to the "assembly" subdirectory under your Windows directory. The .NET Framework Configuration Tool can be found in the Administrative Tools section of the Control Panel.

Figure 4 shows a view of the global assembly cache using the Shell Extension.

Figure 4. Global assembly cache

Strong names

Strong names are used to enable the stricter naming requirements associated with shared assemblies. Strong names have three goals:

  • Name uniqueness. Shared assemblies must have names that are globally unique.
  • Prevent name spoofing. Developers don't want someone else releasing a subsequent version of one of your assemblies and falsely claim it came from you, either by accident or intentionally.
  • Provide identity on reference. When resolving a reference to an assembly, strong names are used to guarantee the assembly that is loaded came from the expected publisher.

Strong names are implemented using standard public key cryptography. In general, the process works as follows: The author of an assembly generates a key pair (or uses an existing one), signs the file containing the manifest with the private key, and makes the public key available to callers. When references are made to the assembly, the caller records the public key corresponding to the private key used to generate the strong name. Figure 5 outlines how this process works at development time, including how keys are stored in the metadata and how the signature is generated.

The scenario is an assembly called "Main," which references an assembly called "MyLib." MyLib has a shared name. The important steps are described as follows.

Figure 5. Process for implementing shared names

  1. The developer invokes a compiler passing in a key pair and the set of source files for the assembly. The key pair is generated with an SDK tool called SN. For example, the following command generates a new key pair and saves it to a file:

    Sn –k MyKey.snk
    The key pair is passed to the compiler using the custom attribute 
            System.Reflection.AssemblyKeyFileAttribute as follows:
    
       <assembly:AssemblyKeyFileAttribute("TestKey.snk")>
    
  2. When the compiler emits the assembly, the public key is recorded in the manifest as part of the assembly's identity. Including the public key as part of identity is what gives the assembly a globally unique name.

  3. After the assembly has been emitted, the file containing the manifest is signed with the private key. The resulting signature is stored in the file.

  4. When Main is generated by the compiler, MyLib's public key is stored in Main's manifest as part of the reference to MyLib.

At run time, there are two steps the .NET Framework takes to ensure that strong names are giving the developer the required benefits. First, MyLib's strong name signature is verified only when the assembly is installed into the global assembly cache—the signature is not verified again when the file is loaded by an application. If the shared assembly is not deployed to the global assembly cache, the signature is verified every time the file is loaded. Verifying the signature ensures that the contents of MyLib have not been altered since the assembly was built. The second step is to verify that the public key stored as part of Main's reference to MyLib matches the public key that is part of MyLib's identity. If these keys are identical, the author of Main can be sure the version of MyLib that was loaded came from the same publisher that authored the version of MyLib with which Main was built. This key equivalence check is done at run time, when the reference from Main to MyLib is resolved.

The term "signing" often brings Microsoft Authenticode® to mind. It is important to understand that strong names and Authenticode are not related in any way. The two techniques have different goals. In particular, Authenticode implies a level of trust associated with a publisher, while strong names does not. There are no certificates or third-party signing authorities associated with strong names. Also, strong name signing is often done by the compiler itself as part of the build process.

Another consideration worth noting is the "delay signing" process. It is often the case that the author of an assembly doesn't have access to the private key needed to do the full signing. Most companies keep these keys in well-protected stores that can only be accessed by a few people. As a result, the .NET Framework provides a technique called "delay signing" that allows a developer to build an assembly with only the public key.In this mode, the file isn’t actually signed because the private key isn’t supplied. Instead, the file is signed later using the SN utility. See Delay Signing an Assembly in the .NET Framework SDK for details on how to use delay signing.

Version Policy

As just described, each assembly manifest records information about the version of each dependency it was built against. However, there are some scenarios in which the application author or administrator may wish to run with a different version of a dependency at run time. For example, administrators should be able to deploy bug fix releases without requiring that every application be recompiled in order to pick up the fix. Also, administrators must be able to specify that a particular version of an assembly never be used if a security hole or other severe bug is found. The .NET Framework enables this flexibility in version binding through version policies.

Assembly Version Numbers

Each assembly has a four-part version number as part of its identity (that is, version 1.0.0.0 of some assembly and version 2.1.0.2 are completely different identities as far as the class loader is concerned). Including the version as part of the identity is essential to distinguish different versions of an assembly for the purposes of side-by-side.

The parts of the version number are major, minor, build and revision. There are no semantics applied to the parts of the version number. That is, the CLR does not infer compatibility or any other characteristic of an assembly based on how the version number is assigned. As a developer you are free to change any portion of this number as you see fit. Even though there are no semantics applied to the format of the version number, individual organizations will likely find it useful to establish conventions around how the version number is changed. This helps maintain consistency throughout an organization and makes it easier to determine things like which build a particular assembly came from. One typical convention is as follows:

Major or minor. Changes to the major or minor portion of the version number indicate an incompatible change. Under this convention then, version 2.0.0.0 would considered incompatible with version 1.0.0.0. Examples of an incompatible change would be a change to the types of some method parameters or the removal of a type or method altogether.

Build. The Build number is typically used to distinguish between daily builds or smaller compatible releases.

Revision. Changes to the revision number are typically reserved for an incremental build needed to fix a particular bug. You'll sometimes hear this referred to as the "emergency bug fix" number in that the revision is what is often changed when a fix to a specific bug is shipped to a customer.

Default Version Policy

When resolving references to shared assemblies, the CLR determines which version of the dependency to load when it comes across a reference to that assembly in code. The default version policy in .NET is extremely straightforward. When resolving a reference, the CLR takes the version from the calling assembly’s manifest and loads the version of the dependency with the exact same version number. In this way, the caller gets the exact version that he was built and tested against. This default policy has the property that it protects applications from the scenario where a different application installs a new version of a shared assembly that an existing application depends on. Recall that before .NET, existing applications would start to use the new shared component by default. However, in .NET, the installation of a new version of a shared assembly does not affect existing applications.

Custom Version Policy

There may be times when binding to the exact version the application was shipped with isn’t what you want. For example, an administrator may deploy a critical bug fix to a shared assembly and want all applications to use this new version regardless of which version they were built with. Also, the vendor of a shared assembly may have shipped a service release to an existing assembly and would like all applications to begin using the service release instead of the original version. These scenarios and others are supported in the .NET Framework through version policies.

Version policies are stated in XML files and are simply a request to load one version of an assembly instead of another. For example, the following version policy directs the CLR to load version 5.0.0.1 instead of version 5.0.0.0 of an assembly called MarineCtrl:

 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  <dependentAssembly
   <assemblyIdentity name="MarineCtrl" publicKeyToken="9335a2124541cfb9" />
      <bindingRedirect oldVersion="5.0.0.0" newVersion="5.0.0.1" />   
  </dependentAssembly>
</assemblyBinding>

In addition to redirecting from a specific version number to another, you can also redirect from a range of versions to another version. For example, the following policy redirects all versions from 0.0.0.0 through 5.0.0.0 of MarineCtrl to version 5.0.0.1:

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  <dependentAssembly
   <assemblyIdentity name="MarineCtrl" publicKeyToken="9335a2124541cfb9" />
      <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.1" />   
  </dependentAssembly>
</assemblyBinding>

Version Policy Levels

There are three levels at which version policy can be applied in .NET: application-specific policy, publisher policy and machine-wide policy.

Application-specific Policy. Each application has an optional configuration file that can specify the application’s desire to bind to a different version of a dependent assembly. The name of the configuration file varies based on the application type. For executable files, the name of the configuration file is the name of the executable + a ".config" extension. For example, the configuration file for "myapp.exe" would be "myapp.exe.config". Configuration files for ASP.NET applications are always "web.config".

Publisher Policy. While application-specific policy is set either by the application developer or administrator, publisher policy is set by the vendor of the shared assembly. Publisher policy is the vendor’s statement of compatibility regarding different versions of her assembly. For example, say the vendor of a shared Windows Forms control ships a service release that contains a number of bug fixes to the control. The original control was version 2.0.0.0 and the version of the service release is 2.0.0.1. Because the new release just contains bug fixes (no breaking API changes) the control vendor would likely issue publisher policy with the new release that causes existing applications that used 2.0.0.0 to now start using 2.0.0.1. Publisher policy is expressed in XML just as application and machine-wide policy are, but unlike those other policy levels, publisher policy is distributed as an assembly itself. The primary reason for this is to ensure that the organization releasing the policy for a particular assembly is the same organization that released the assembly itself. This is accomplished by requiring that both the original assembly and the policy assembly are given a strong name with the same key-pair.

Machine-wide Policy. The final policy level is machine-wide policy (sometimes referred to as Administrator policy). Machine-wide policy is stored in machine.config which is located in the "config" subdirectory under the .NET Framework install directory. The install directory is %windir%\microsoft.net\framework\%runtimeversion%. Policy statements made in machine.config affect all applications running on the machine. Machine-wide policy is used by Administrators to force all applications on a given machine to use a particular version of an assembly. The most comment scenario in which this is used is when a security or other critical bug fix has been deployed to the global assembly cache. After deploying the fixed assembly, the Administrator would use machine-wide version policy to ensure that applications don’t use the old, broken version of the assembly.

Policy Evaluation

The first thing the CLR does when binding to a strongly named assembly is determine which version of the assembly to bind to. The process starts by reading the version number of the desired assembly that was recorded in the manifest of the assembly making the reference. Policy is then evaluated to determine if any of the policy levels contain a redirection to a different version. The policy levels are evaluated in order starting with application policy, followed by publisher and finally administrator.

A redirection found at any level overrides any statement made by a previous level. For example, say that assembly A references assembly B. The reference to B in A’s manifest is to version 1.0.0.0. Furthermore, the publisher policy shipped with assembly B redirects the reference from 1.0.0.0 to 2.0.0.0. In addition, there is version policy is the machine-wide configuration file that directs the reference to version 3.0.0.0. In this case, the statement made at the machine level will override the statement made at the publisher level.

Bypassing Publisher Policy

Because of the order in which the three types of policy are applied, the publisher’s version redirect (publisher policy) can override both the version recorded in the calling assembly’s metadata and any application-specific policy that was set. However, forcing an application to always accept a publisher’s recommendation about versioning can lead back to DLL Hell. After all, DLL Hell is primarily caused by the difficulty of maintaining backwards compatibility in shared components. To further avoid the scenario where an application is broken by the installation of a new version of a shared component, the version policy system in .NET allows an individual application to bypass publisher policy. In other words, an application can refuse the take the publisher’s recommendation about which version to use. An application can bypass publisher policy using the "publisherPolicy" element in the application configuration file:

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<publisherPolicy apply="no"/>

</assemblyBinding>

Setting Version Policies with the .NET Configuration Tool

Fortunately, the .NET Framework ships with a graphical admin tool so you don't have to worry about hand editing XML policy files. The tool supports both application and machine-wide version policy. You’ll find the tool in the Administrative Tools section of Control Panel. The initial screen of the admin tool looks like Figure 6:

Figure 6. The Admin tool

The following steps describe how to set application-specific policy:

  1. Add your application to the Applications node in the tree view. Right-click the Applications node and click Add. The Add dialog box shows a list of .NET applications to pick from. If your application isn’t in the list, you can add it by clicking Other.

  2. Choose the assembly you’d like to set policy for. Right-click the Configured Assemblies node and click Add. One of the options is to pick an assembly from the list of assemblies that the application references, and it displays the following dialog as shown in Figure 7. Pick an assembly and click Select.

    Figure 7. Choosing an assembly

  3. In the Properties dialog box, enter the version policy information. Click the Binding Policy tab and enter the desired version numbers in the table as shown in Figure 8.

    Figure 8. Binding Policy tab

Deployment

Deployment involves at least two different aspects: packaging the code, and distributing the packages to the various clients and servers on which the application will run. A primary goal of the .NET Framework is to simplify deployment (especially the distribution aspect) by making zero-impact install and xcopy deployment feasible. The self-describing nature of assemblies allows us to remove our dependency on the registry, thereby making install, uninstall, and replication much simpler. However, there are scenarios where xcopy is not sufficient or appropriate as a distribution mechanism. For these cases, the .NET Framework provides extensive code download services and integration with the Windows Installer.

Packaging

There are three packaging options available in the first release of the .NET Framework:

  • As-built (DLLs and EXEs). In many scenarios, no special packaging is required. An application can be deployed in the format produced by the development tool. That is, a collection of DLLs and EXEs.
  • Cab files. Cab files can be used to compress your application for more efficient downloads.
  • Windows Installer packages. Microsoft Visual Studio .NET and other installation tools allow you to build Windows Installer packages (.msi files). The Windows Installer allows you to take advantage of application repair, on-demand install, and other Microsoft Windows application-management features.

Distribution Scenarios

.NET applications can be distributed in a variety of ways, including xcopy, code download, and through the Windows Installer.

For many applications, including Web applications and Web Services, deployment is as simple as copying a set of files to disk and running them. Uninstall and replication are just as easy—just delete the files or copy them.

The .NET Framework provides extensive code download support using a Web browser. Several improvements have been made in this area, including:

  • Zero-impact. No registry entries are made on the machine.
  • Incremental download. Pieces of an assembly are downloaded only as they are referenced.
  • Download isolated to the application. Code downloaded on behalf of one application cannot affect others on the machine. A primary goal of our code download support is to prevent the scenario where a user downloads a new version of some shared component as a side effect of browsing to a particular Web site and having that new version adversely affect other applications.
  • No Authenticode dialogs. The code access security system is used to allow mobile code to run with a partial level of trust. Users will never be presented with dialog boxes asking them to make a decision about whether they trust the code.

Finally, .NET is fully integrated with the Windows Installer and the application management features of Windows.

Summary

The .NET Framework enables zero-impact install and addresses DLL Hell. Assemblies are the self-describing, versionable deployment units used to enable these features.

The ability to create isolated applications is crucial because it allows applications to be built that won't be affected by changes made to the system by other applications. The .NET Framework encourages this type of application through application-private assemblies that are deployed within the directory structure of the application.

Side by side is a core part of the sharing and versioning story in .NET. Side by side allows multiple versions of an assembly to be installed and running on the machine simultaneously, and allows each application to request a specific version of that assembly.

The CLR records version information between pieces of an application and uses that information at run time to ensure that the proper version of a dependency is loaded. Version policies can be used by both application developers and administrators to provide some flexibility in choosing which version of a given assembly is loaded.

There are several packaging and distribution options provided by the .NET Framework, including the Windows Installer, code download, and simple xcopy.