Assemblies are binaries used to package and distribute executable code. There are similar to Win32 DLLs and EXEs in functionality, but they provide much more functionality than their Win32 DLLs and EXE counterpart. Specifically, assemblies provide the following functions:
Assemblies can be either static or dynamic. Static assemblies are the ones you usually deal with - they are stored and loaded form the disk. Dynamic assemblies on the other hand are usually created using Reflection.Emit and are run directly from memory are not saved to disk before execution.
As assembly consists of one or more physical files called modules. A modules in an executable file containing code and metadata. Each module may be produced using different languages. Also note that only one module in an assembly contains the manifest.
To solve problems associated with DLL Hell and versioning problems, the CLR uses assemblies to do the following:
Because assemblies are self-describing deployment units that have no dependencies on registry entries, they enable zero-impact application installation. They also simplify uninstalling and replicating applications.
In general, a static assembly (the regular assemblies generated when compiling your code) consists of four elements:
There are several ways to group these elements in an assembly. In the usual scenario, all these elements are embedded within a single assembly. Alternatively, these elements can be contained in several files (multi-file assembly). See MSDN for more details on multi-file assemblies.
The assembly manifest is a collection of data that describe how elements in the assembly relate to each other. Assemblies, whether static or dynamic, always contain a manifest. Specifically, an assembly's manifest contains the following information:
Each assembly's manifest performs the following functions:
The following table shows the kind of information you can expect to find in an assembly's manifest. The first four items - assembly name, version number, culture, and strong name - make up the assembly's identity:
Information | Description |
Assembly Name | A text describing assembly's name |
Version | Major.Minor.Revision.Build number. This number is used to enforce versioning policy. |
Culture | Information on the culture or language supported by the assembly. This information should only be used to designate an assembly as a satellite assembly containing language or culture-specific information. |
Strong Name | The public key from the publisher if the assembly has been given a strong name (sn.exe utility) |
List of all files in the assembly | A hash of each file contained in the assembly and its corresponding file name |
Type reference information | Information used by the runtime to map a type reference to the file that contains its declaration and implementation. |
Information on referenced assemblies | A list of other assemblies that this assembly depends on. Note that this information includes the dependent assembly's version and public key (among others) |
Informational attributes | Generic text-based information such as company name, trademark, and other attributes used |
The following shows the actual information contained in a DLL's manifest:
// Information on referenced
assemblies
.assembly extern System
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:5000:0
}
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:5000:0
}
.assembly extern System.Drawing
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 1:0:5000:0
}
.assembly extern System.Windows.Forms
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:5000:0
}
.assembly extern System.Runtime.Serialization.Formatters.Soap
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 1:0:5000:0
}
.assembly Components
{
// Informational attributes
.custom instance void [mscorlib]System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 00 00 00 )
// --- The following custom attribute is added automatically, do not uncomment -------
// .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
// bool) = ( 01 00 01 01 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 )
.hash algorithm 0x00008004
// Version number
.ver 1:0:1574:31910
}
.mresource public Components.MyComponent.resources
{
}
.mresource public Components.Interfaces.resources
{
}
// Assembly Name
.module Components.dll
// MVID: {1CF22289-72C4-471D-A6F9-CB15F9FDA96E}
.imagebase 0x11000000
.subsystem 0x00000003
.file alignment 4096
.corflags 0x00000001
// Image base: 0x03320000
The Global Assembly Cache (GAC) is a machine-wide cache of code that stores assemblies specifically designated to be shared across multiple applications. The general rule on using the GAC is:
Unless sharing an assembly is explicitly required, keep assembly dependencies private and locate assemblies in the application's directory structure. Install assemblies in the GAC only when you need to share them.
There are several ways to deploy an assembly the GAC:
Assemblies deployed in the GAC must have a strong name. When as assembly is added to the GAC, integrity checks are performed on all files that make up the assembly to ensure that the assembly has not been tampered with.
All assemblies have a four-part name that uniquely identify the locale and developer of the component:
For example, the following code (found in AssemblyInfo.cs or .vb):
using System.Reflection
[assembly:AssemblyVersion("1.2.3.4")]
[assembly:AssemblyCulture("en-US")]
[assembly:AssemblyKeyFile("MyKey.snk")]
results in the following full assembly name
MyComponent, Version=1.2.3.4, Culture=en-US, PublicKeyToken=1234567812345678
A strong name consists of three main components:
Strong names can be generated using the sn.exe command-line too. Assemblies having the same strong name are expected to be identical. Strong-named assemblies satisfy the following requirements:
Note that strong-named assemblies can only reference other strong-named assemblies. The following shows the 2 processes for generating strong-named assemblies.
As assembly can be signed in two different but complementary ways: using sn.exe or using signcode.exe. Signing an assembly with sn.exe adds a public key encryption to the file containing the assembly's manifest. However, no level of trust is associated with a strong name. Enter signcode.exe. Signcode requires an assembly developer/publisher to prove their identity to a third-party authority and obtain a certificate. signcode.exe embeds this certificate into your assembly. The administrator installing your assembly can then decide whether to trust your assembly or not based on its certificate.
Note that you give an assembly both a strong name and a digital signature, or you can use either alone. A strong name is stored in the file containing the assembly's manifest. Likewise, signcode signs the file containing the assembly's manifest. When you assign a strong name and sign with signcode, the strong name must be given first.
Because strong naming and signcoding guarantee integrity, you can base code access security of these two forms of assembly evidence.
Assemblies can be loaded dynamically using either an explicit URL denoting the assembly's location or the four-part full assembly name.
The following code shows how Assemblies.LoadFrom loads an assembly:
using System;
using System.Reflection;
public void LoadAssembly( string strURL, string strClassName )
{
// assume strURL is:
file://c:/Projects/Components.dll
Assembly a = Assembly.LoadFrom( strURL );
// assume strClassName is:
MyProjects.MyComponents.Component1
object o = a.CreateInstance( strClassName );
}
Because the URL specifies the exact location of the file, Assemblies.LoadFrom is straightforward. However, Assemblies.Load is different and goes through the following process to resolve and load the assembly:
This process and the relevant elements shown above will be explained below.
Recall that an assembly version number is represented using the following format:
<Major>.<Minor>.<Build>.<Revision>
Two assemblies that differ by their version number are considered two different assemblies with respect to versioning. However, recall that versioning only applies to strongly-named assemblies. The following table shows the actual version numbers for common short cuts seen in the AssemblyVersion attribute:
AssemblyVersion Value | Meaning |
1 | 1.0.0.0 |
1.2 | 1.2.0.0 |
1.2.3 | 1.2.3.0 |
1.2.3.4 | 1.2.3.4 |
1.2.* | 1.2.d.s |
1.2.3.* | 1.2.3.s |
not present | 0.0.0.0 |
Where d is the number of days since Feb 1, 2000 and s is the number of seconds since midnight / 2.
An assembly's version and the versions of its dependent assemblies are stored in the manifest. An assembly has two ways of expression its version information:
Note these two important points regarding assembly versions:
For example, assume assembly A has version 1.0.0.1 and assembly B has version 1.0.0.1 and that assembly A lists assembly B as a dependent. If both assemblies have strong-names, then version 1.0.0.1 of A will only run with version 1.0.0.1 of B. If both assemblies do not have a strong name, then any version of A can run with any version of B. The situation where one assembly has a strong name and the other does not is not a valid scenario because strong-named assemblies require then dependent assemblies to have a strong-name
The runtime performs several steps when assembly A decides to call the dependent assembly B:
For example, step 1 may say that assembly A needs to call version 5 of assembly B (according to A's manifest). Step 2 however, may determine that the version policy requires version 6 of assembly B. Step 3 determines the required version to use is 6, and step 4 then searches for version 6 of assembly B. The discussion that follows relate to points 2 and 3 above where the correct assembly version is determined.
Therefore, the mechanism can be summarized as:
As steps 2 and 3 above imply, the assembly resolver (a CLR component that locates the correct assembly) can map the requested version in step 1 to a newer or older version. The following shows how to create an assembly Version Policy within the application's configuration file:
<configuration>
<runtime>
<!-- 0 or 1 assemblyBinding element
Contains information about assembly location and assembly version redirection -->
<assemblyBinding>
<!-- 0 or 1 probing element
Specifies subdirectories below the application base direction that the CLR should search
when loading assemblies -->
<probing
privatePath = "" />
<!-- 0 or 1 publisherPolicy element. Specifies whether the runtime applies publisher policy -->
<publisherPolicy
apply="" />
<!-- 0 to N dependentAssembly elements. Encapsulates binding policy and location for
each assembly -->
<dependentAssembly>
<!-- self-explanator -->
<assemblyIdentity name=""
publicKeyToken="" culture=""/>
<!-- Specifies where the CLR can find the assembly.
Once codebase per version-->
<codeBase version=""
href=""/>
<!-- Redirect a specific version to another. This
element can be repeated many times -->
<bindingRedirect oldVersion="" newVersion="" />
<!-- Specifies whether the runtime applies publisher policy
-->
<publisherPolicy apply=""/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
The following code fragment redirects references to version 1.0.0.0 of a shared assembly called MyComponent.dll to version 2.0.0.0:
<configuration>
<runtime>
<assemblyBinding xmlns="urs:X">
<dependentAssembly>
<assemblyIdentity name="DataAccess" publicKeyToken="1234abcd1234abcd" culture=""/>
<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
In addition to version policies provided at the application level, the .NET Framework provides two other policy levels: publisher and administrator:
The three policy levels all have the same format described above, however a publisher policy even though it is specified as an XML configuration file, it is usually shipped as an assembly. There are three reasons that publisher policies are shipped as assemblies:
The flexibility obtained by decoupling compatibility statements from code makes it much easier to fix broken applications. If multiple versions of a publisher policy assembly are found in the assembly cache, the one with the highest version number will be used.
There are two steps to create a publisher policy assembly:
To create an assembly from a configuration file:
AL /link:MyComponentPolicy.xml /out:MyComponentPolicy2.0.0.0 /keyfile:MyComponent.snk /version:2.0.0.0
Once the correct version has been determined, the CLR now attempts to locate and bind to the the required assembly. The term probing is often used when describing how the runtime attempts to locate assemblies. The runtime uses the following steps to resolve an assembly references:
If the requested assembly has also been requested in previous calls, the CLR will use the assembly that is already loaded.
The binding process continues for strong-named assemblies by looking in the GAC. Note that the GAC is a secured resource that requires administrator rights to add and delete entries. Also note that the GAC can only contain signed assemblies with public keys.
After the CLR determines the correct assembly version by looking in the assembly's manifest and then consulting the appropriate policy versions, and after looking in the GAC (for strong-named assemblies), the CLR attempts to find the assembly using the following order:
<configuration>
<runtime>
<assemblyBinding xmlns="urs:X">
<dependentAssembly>
<assemblyIdentity name="DataAccessAsync" publicKeyToken="5678abcd5678abcd" culture=""/>
<codebase version="1.0.0.0" href="file://c:/MyApp/DataAccessAsync.dll" />
<codebase version="1.1.0.0" href="file://c:/MyApp/new/DataAccessAsync.dll
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Criteria | Meaning | Notes |
Application Base | Root location where the application is being executed. The root location can be either be a URL or a directory. | If the assembly is not
found in the application base and no culture information is
provided, the CLR searches any subdirectories with the assembly
name. See example 1.
If the assembly is not found in the application base and culture information is provided, the CLR searches any subdirectories with the culture name . See example 2. |
Culture | Culture attribute of the referenced assembly | |
Name | Name of the referenced assembly | |
Private binpath | A user-defined list of sub-directories under the root location. This location can be specified in the application's configuration file using the <probing> element and in managed code using the AppendPrivatePath property | If culture was included, see example 3. If culture was not included see example 4. |
[application base] / [assembly name].dll
[application base] / [assembly name] / [assembly name].dll
Example 2
[application base] / [culture] / [assembly name].dll
[application base] / [culture] / [assembly name] / [assembly name].dll
[application base] / [binpath] / [culture] / [assembly
name].dll
[application base] / [binpath] / [culture] / [assembly name] / [assembly
name].dll
[application base] / [binpath] / [assembly name].dll
[application base] / [binpath] / [assembly name] / [assembly name].dll
<configuration>
<runtime>
<assemblyBinding xmlns="urs:X">
<probing
privatePath="bin;shared" />
</assemblyBinding>
</runtime>
</configuration>
The following diagram summarizes the assembly probing process:
Side-by-side execution is the ability to run multiple versions of the same assembly simultaneously. For example, consider class DataAccess with has versions 1 and 2. Callers of DataAccess that express a dependency on version 1 will always get version 1 regardless of many subsequent version of DataAccess are installed on the computer. Other callers that express no specific dependency can always get the latest version of DataAcess.
Support for side-by-side storage and side-by-side execution of different versions of the same assembly is built into the infrastructure of the runtime. Side-by-side storage can be accomplished via the use of the GAC. As for side-by-side execution, there are distinct types: