All files that comprise a .NET application are gathered into one or more assemblies. Assemblies are the basic unit of .NET programming. They appear to the user as dynamic link library (DLL) files or an executable (EXE) file. (DLLs contain classes and methods that are linked into an application at runtime as needed.)
Assemblies also contain versioning information. An assembly is the minimum unit for a single version of a piece of code. Multiple versions of the same code can run side by side in different applications, with no conflicts, by packaging the different versions into separate assemblies and specifying which version is allowed in the configuration files.
Assemblies can contain one or more files. The constituent files in an assembly are portable executable files such as EXEs or DLLs.
22.3.1 Microsoft Intermediate Language (MSIL)
When a .NET application is compiled into an executable file, that file does not contain machine code (unless it was compiled with the Managed C++ compiler), as is the case with most other language compilers. Instead, the compiler output is a language known as Microsoft Intermediate Language (MSIL), or IL for short. When the program is run, the .NET Framework calls a Just-In-Time (JIT) compiler to compile the IL into machine code, which is then executed. The JITed machine code is cached on the machine, so there is no need to recompile with every execution.
In theory, a program will produce the same IL code if written with any .NET compliant language. While this is not always true in practice, it is fair to say that for all practical purposes, all the .NET languages are equivalent. This is evident from the examples in this book that are presented in both C# and VB.NET.
Using IL offers several advantages. First, it allows the JIT compiler to optimize the output for the platform. As of this writing, the .NET platform is supported on Windows environments running on Intel Pentium-compatible processors, and the .NET Compact Framework runs on ARM processors. It is not a stretch to imagine the Framework being ported to other operating environments, such as Linux or Mac OS, or other hardware platforms. Even more likely, as new generations of processor chips become available, Microsoft could release new JIT compilers that detect the specific target processor and optimize the output accordingly.
The second major advantage of an IL architecture is that it enables the language-neutral nature of the .NET Framework. Language choice is no longer dictated by the capabilities of one language over another, but by the preferences of the developer or the team. It is even possible to mix languages in a single application. A class written in C# can be derived from a VB.NET class, and an exception thrown in a C# method can be caught in a VB.NET method.
A third advantage is that the CLR can analyze code to determine compliance with requirements such as type safety. Things like buffer overflows and unsafe casts can be caught and disallowed, greatly enhancing security and stability.
22.3.1.1 ILDASM
It is possible to examine the contents of a compiled .NET EXE or DLL by using Intermediate Language Disassembler (ILDASM), a tool provided as part of the .NET SDK. ILDASM parses the contents of the file, displaying its contents in human-readable format. It shows the IL code, as well as namespaces, classes, methods, and interfaces.
|
This useful tool is not installed as part of any menu, either on the Start menu or inside Visual Studio .NET, although it can be added to either. To run ILDASM, open a .NET command line by clicking the Start button, and then Programs Microsoft Visual Studio .NET 2003 Visual Studio .NET Tools Visual Studio .NET 2003 Command Prompt. Then enter:
ildasm
to open the program. Click on File Open to open the file you wish to look at or drag the file from Windows Explorer onto the ILDASM window. Alternatively, enter:
ildasm
where the full path (optional if the exe or dll is in the current directory) and name of the exe or dll is given as an argument. In either case, you will get something similar to that shown in Figure 22-3. You can click on the plus sign next to each node in the tree to expand that node, and then drill down through the file.
Figure 22-3. ILDASM
In Figure 22-3, the examined file is called csConfigDeploy.exe. You can see that it has a Manifest (described later in Section 22.3.3), there is a namespace called csConfigDeploy containing a class called Form1, and Form1 contains several fields and methods.
The icons used in ILDASM are listed in Table 22-1. The colors in which the icons are displayed are also listed.
Icon |
Description |
---|---|
Namespace (blue icon with red top edge) |
|
Class (blue icon) |
|
Interface (blue icon with yellow letter I) |
|
Value class (yellow icon) |
|
Enum (yellow icon with purple letter E) |
|
Method (pink icon) |
|
Static method (pink icon with yellow letter S) |
|
Field (aqua icon) |
|
Static field (aqua icon with dark blue letter S) |
|
Event (green icon) |
|
Property (red icon) |
|
Manifest or class info item (red icon) |
To add ILDASM to the Visual Studio .NET menu, click on the Tools External Tools menu item. You will get the External Tools dialog box. Click on the Add button, fill in the fields with the values shown in Table 22-2, and then click OK.
Field |
Value |
---|---|
Title |
ILDASM |
Command |
C:Program FilesMicrosoft Visual Studio .NET 2003SDKv1.1Binildasm.exe[1] |
Arguments |
$(TargetPath) |
[1] The exact location of ILDASM.exe may vary from installation to installation. It may be necessary to search for the file on your specific machine.
The Initial Directory field can be left blank.
Now when you go into Visual Studio .NET and click on the Tools menu item, ILDASM will be listed. Clicking on that item will open ILDASM with the executable from the current project. If you also want to open ILDASM without any file, add another external tool entry with a different Title (say "ILDASM - No file"), and leave the Arguments field blank.
22.3.2 Modules
A module is a unit of code that can be loaded and run by the CLR. All assemblies contain at least one, although it is possible for an assembly to contain multiple modules. Modules are compiled from your source code, by using the language-specific command-line compiler or the Build command in Visual Studio .NET. To be deployed, a module must be contained within an assembly.
|
Modules use a format that is an extension of the Windows Portable Executable (PE) format. This format applies to DLLs and EXEs, files that may be linked together at runtime to form an executable program. All PE files contain a field that is the location of the code to jump to when the operating system loads the executable. .NET modules don't actually contain any executable code, but contain intermediate language (IL) code, described previously. This IL code must be compiled at runtime by the CLR just-in-time (JIT) compiler. Hence, the location of the code to jump to is inside the CLR, which calls the JIT compiler, and then that JITed code is executed.
22.3.3 Manifests
Assemblies in .NET are self-describing: they contain metadata, which describe the files contained in the assembly and how they relate to one another (references to types, for example), version and security information relevant to the assembly, and dependencies on other assemblies. This metadata is contained in the assembly manifest. Each assembly must have exactly one assembly manifest. Each module has its own module manifest, which contains the metadata pertaining only to that module.
Looking back at Figure 22-3, you can see that a manifest in the file. Double-clicking on the manifest in ILDASM brings up a window that displays the contents of the manifest, as shown in Figure 22-4.
Figure 22-4. Manifest in ILDASM
Looking at the manifest displayed in Figure 22-4, you can see that several external assemblies are referenced, including System.Windows.Forms, System, mscorlib, System.Drawing, and csConfigDeployLibrary. All except the last one are part of the .NET Framework. This assembly itself is referred to with the following section:
.assembly csConfigDeploy { ... }
All of these assemblies have version attributes, and the Framework assemblies also have public key token attributes. Both attributes will be discussed shortly.
22.3.4 Single- and Multimodule Assemblies
Assemblies created by Visual Studio .NET are always single modules contained in a single file with an extension of either EXE for a directly executable file or DLL for a library file. In either case, the manifest is contained within the file.
You can create an assembly that consists of multiple modules, but (at least for now) they must be created from the command line.
|
A multimodule assembly might be desirable for several reasons:
On the other hand, it is advantageous to have a single module assembly in other circumstances:
To demonstrate both single- and multimodule assemblies, you will create a simple application in Visual Studio .NET comprised of a main EXE and a class library contained in a DLL. Both the EXE and the DLL will be in their own single-module assembly, and the EXE will reference the assembly containing the DLL. Then you will compile the same application from the command line as a monolithic multimodule assembly.
22.3.4.1 Single-file assemblies
Open Visual Studio .NET and create a new Windows Application in the language of your choice. Name it Deploy.
|
Right-click on the Solution in Solution Explorer and select Add and then New Project..., which will bring up the Add New Project dialog box. Click on the Class Library template and name the new project DeployLibrary. You should look at the code window for Class1.cs or Class1.vb.
In the Solution Explorer, right-click on References in the DeployLibrary project and select Add Reference, bringing up the Add Reference dialog box. In the .NET tab, slide down to locate System.Windows.Forms and double-click on it, which will add it to the Selected Components grid at the bottom of the dialog box. Click OK.
At the top of the code window for Class1.cs/vb, add a reference to the System.Windows.Forms namespace. In C#:
using System.Windows.Forms;
and in VB.NET:
imports System.Windows.Forms
Now add a method to the Class1 class in DeployLibrary called SayHi. This method will be static (shared in VB.NET) and return nothing (void in C# and a sub in VB.NET) and take a single string argument. This method will put up a message box displaying the string argument.
public static void SayHi(string strMsg) { MessageBox.Show(strMsg, "csConfigDeploy.Class1"); }
public shared sub SayHi(strMsg as String) MessageBox.Show(strMsg, "vbConfigDeploy.Class1") end sub
Notice that the MessageBox.Show has a second argument, the message box title, which is hardwired to the name of the class.
The VB.NET version of the project so far should look something like in Figure 22-5.
Figure 22-5. DeployLibrary in VB.NET
The C# version will look very similar to the VB.NET code shown in Figure 22-5. The complete C# code listing for DeployLibrary is shown in Example 22-10.
Example 22-10. DeployLibrary in C#
using System; using System.Windows.Forms; namespace csDeployLibrary { ///
/// Summary description for Class1. ///
public class Class1 { public Class1( ) { } public static void SayHi(string strMsg) { MessageBox.Show(strMsg, "csConfigDeploy.Class1"); } } }
You can build the DeployLibrary project at this point to verify that there are no build errors.
Now go back to the design view of the first project, Deploy. Drag a button from the Toolbox onto the form. Change the Text property of the button to Message and rename the button btnMsg.
In Solution Explorer, right-click on the References in the Deploy project and click on Add Reference. In the Add Reference dialog box, click on the Projects tab. The DeployLibrary project should be listed in the top half of the dialog. Double-click on it to add it to the bottom half of the dialog, and then click OK.
Double-click on the button on the form, which will create a skeleton for the button click event handler and bring you to the code window. Inside the event handler, enter a single line of code (identical for both languages, except the trailing semicolon in C#):
csDeployLibrary.Class1.SayHi("Is this cool or what?");
Notice as you type that when you enter the first period, IntelliSense pops up all the classes in the library (there is only one), and as you type the next period, IntelliSense again pops up all the methods, including SayHi.
Now when you run the project, a form displays with a single button. Clicking the button pops up a dialog box with your message, as shown in Figure 22-6.
Figure 22-6. Deploy application with message box
The complete code listing from Visual Studio .NET (minus the autogenerated comments) for the Deploy project is listed in Example 22-11 in C# and in Example 22-12 in VB.NET.
Example 22-11. Deploy project in C# (Form1.cs)
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; namespace csDeploy { public class Form1 : System.Windows.Forms.Form { private System.Windows.Forms.Button btnMsg; private System.ComponentModel.Container components = null; public Form1( ) { InitializeComponent( ); } protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose( ); } } base.Dispose( disposing ); } #region Windows Form Designer generated code private void InitializeComponent( ) { this.btnMsg = new System.Windows.Forms.Button( ); this.SuspendLayout( ); // // btnMsg // this.btnMsg.Location = new System.Drawing.Point(80, 80); this.btnMsg.Name = "btnMsg"; this.btnMsg.TabIndex = 0; this.btnMsg.Text = "Message"; this.btnMsg.Click += new System.EventHandler(this.btnMsg_Click); // // Form1 // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(292, 273); this.Controls.AddRange(new System.Windows.Forms.Control[ ] { this.btnMsg}); this.Name = "Form1"; this.Text = "Form1"; this.ResumeLayout(false); } #endregion [STAThread] static void Main( ) { Application.Run(new Form1( )); } private void btnMsg_Click(object sender, System.EventArgs e) { csDeployLibrary.Class1.SayHi("Is this cool or what?"); } } }
Example 22-12. Deploy project in VB.NET (Form1.vb)
Public Class Form1 Inherits System.Windows.Forms.Form #Region " Windows Form Designer generated code " Public Sub New( ) MyBase.New( ) InitializeComponent( ) End Sub Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose( ) End If End If MyBase.Dispose(disposing) End Sub Private components As System.ComponentModel.IContainer Friend WithEvents btnMsg As System.Windows.Forms.Button Private Sub InitializeComponent( ) Me.btnMsg = New System.Windows.Forms.Button( ) Me.SuspendLayout( ) ' 'btnMsg ' Me.btnMsg.Location = New System.Drawing.Point(96, 88) Me.btnMsg.Name = "btnMsg" Me.btnMsg.TabIndex = 0 Me.btnMsg.Text = "Message" ' 'Form1 ' Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) Me.ClientSize = New System.Drawing.Size(292, 273) Me.Controls.AddRange(New System.Windows.Forms.Control( ) {Me.btnMsg}) Me.Name = "Form1" Me.Text = "Form1" Me.ResumeLayout(False) End Sub #End Region Private Sub btnMsg_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnMsg.Click vbDeployLibrary.Class1.SayHi("Is this cool or what?") End Sub End Class
When this application is run in Visual Studio .NET, the DeployLibrary project is compiled into a DLL and placed in the bindebug directory (for C#; for VB.NET, all outputs go in the single bin directory) under the project directory. Looking at that DLL in ILDASM (Figure 22-7), you can see the manifest, Class1, the constructor (.ctor), and the static (Shared in VB.NET) method SayHi.
Figure 22-7. C# version of DeployLibrary.dll in ILDASM
Drilling down into the manifest, you will see the code listed in Example 22-13, with some of the lines truncated or omitted for clarity. The first two highlighted lines indicate references to external assemblies that are part of the .NET Framework. The third highlighted line indicates that this DLL is an assembly, as you would expect from the way it was built.
Example 22-13. Manifest for C# version of DeployLibrary.dll
.assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 1:0:3300:0 } .assembly extern System.Windows.Forms { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 1:0:3300:0 } .assembly csDeployLibrary { // lines omitted for brevity .hash algorithm 0x00008004 .ver 1:0:1137:20914 } .module csDeployLibrary.dll // MVID: {AB7ACB6E-17F9-4BF1-94BF-0E4DDF9EE8D9} .imagebase 0x11000000 .subsystem 0x00000003 .file alignment 512 .corflags 0x00000001 // Image base: 0x03090000
Likewise, opening Deploy.exe in ILDASM and drilling into the manifest reveals the code shown in Example 22-14. This manifest also references several external assemblies, all but one of which are part of the .NET Framework. The exception, csDeployLibrary, is the class library project you created as part of the solution. Again, the last highlighted line of code indicates that csDeploy.exe is itself an assembly.
Example 22-14. Manifest for C# version of Deploy.exe
.assembly extern System.Windows.Forms { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 1:0:3300:0 } .assembly extern System { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 1:0:3300:0 } .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 1:0:3300:0 } .assembly extern System.Drawing { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) .ver 1:0:3300:0 } .assembly extern csDeployLibrary { .ver 1:0:1137:20914 } .assembly csDeploy { // lines omitted for brevity .hash algorithm 0x00008004 .ver 1:0:1137:21381 } .mresource public csDeploy.Form1.resources { } .module csDeploy.exe // MVID: {CC84A390-869D-4DD1-91FF-E323812D19EF} .imagebase 0x00400000 .subsystem 0x00000002 .file alignment 512 .corflags 0x00000001 // Image base: 0x03090000
From the foregoing example, you see that Visual Studio .NET creates one single-module assembly for each DLL or EXE, and then references the assemblies in the manifests as necessary. Deploy.exe knows to reference DeployLibrary.dll because you added a reference in Solution Explorer.
You can verify that the CLR will load the modules only as necessary by running the application in the debugger with the Modules window displayed. (Open the Modules window in the running program by selecting Debug Windows Modules.) In Figure 22-8, the module csDeployLibrary.dll has not yet been loaded because the button was not yet clicked. As soon as the button is clicked and the code in the event handler is executed, that module will appear in the modules window.
Figure 22-8. Modules window in running application
22.3.4.2 Multimodule assemblies
Now you will compile the same application as a single assembly containing multiple modules. This cannot be done from within Visual Studio .NET (except when using Managed C++, which will not be covered in this book), but from the command line. It is a multistep process.
|
First, you compile each source file into individual modules. (Remember, a module is a unit of code that can be included in an assembly.) Then use the Assembly Linker tool (al.exe) to combine all the modules into a single assembly, which can be either a DLL or an EXE. To deploy this assembly, you must include all the constituent module files as well as the assembly file.
22.3.4.2.1 Preparing the source files
Before you begin, create a new directory to hold all the source code and output files. (It doesn't matter what you call it.) Copy Form1.cs or Form1.vb from the Deploy project directory and Class1.cs or Class1.vb from the DeployLibrary project directory to this new directory.
Now some slight modifications need to be made to the source code, which is done easily in a text editor. (All modifications, with one exception, can be made in Visual Studio .NET before you copy the files, since they will not affect the build process in Visual Studio .NET. The exception is the manner in which the SayHi method is called in VB.NET.)
If you work in C#, you must make a very slight modification to the source code from the Deploy project, Form1.cs, adding the keyword public to the declaration for the Main method, which is the entry point for the program.
public static void Main( )
In VB.NET, this is not necessary because the entry point, New( ), is already declared as public by default.
If you are working in VB.NET, you must enclose the library class in a namespace. Edit class1.vb, adding the highlighted lines from Example 22-15 to the code originally shown in Figure 22-5.
Example 22-15. DeployLibrary with added namespace in VB.NET
imports System.Windows.Forms namespace vbDeployLibraryNS Public Class Class1 public shared sub SayHi(strMsg as String) MessageBox.Show(strMsg, "vbConfigDeploy.Class1") end sub End Class End NameSpace
None of the modifications made so far will affect the build of the application in Visual Studio .NET. This next modification will work only when compiling from the command line.
Modify the call to the SayHi method made from the button click event handler within the Deploy project (Form1.vb), originally shown in Example 22-12, replacing it with the highlighted code shown in Example 22-16.
Example 22-16. Modified call to library in Deploy project (Form1.vb)
Private Sub btnMsg_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnMsg.Click vbDeployLibraryNS.Class1.SayHi("Is this cool or what?") End Sub
You must also explicitly add an entry point to the VB.NET form. (In Visual Studio .NET, this is done for you behind the scenes.) Add the Main method listed in Example 22-17 to Form1.vb, somewhere inside the class.
Example 22-17. Entry point in Deploy project (Form1.vb)
public shared sub Main( ) System.Windows.Forms.Application.Run(new Form1( )) end sub
22.3.4.2.2 Compiling the modules
The source files must be compiled into modules by using the language-specific command-line compilers. The /target: argument (/t: for short) to the compiler specifies the type of output file that will be created. The valid values of this argument were listed in Table 2-1 in Chapter 2. For your purposes here, you will use the /target:module argument to output a module that can be added to an assembly later.
You can specify the name of the output file by using the /out: argument. If the target type is module and the /out: argument is omitted, then the output filename will default to the same name as the source file with an extension of .netmodule.
First, compile the library file into a module, and then use the output from that operation to compile the main part of the application into another module. The second module will refer to the library module.
To compile the library file, Class1.cs or Class1.vb, use the following command line:
csc /t:module class1.cs
vbc /r:system.windows.forms.dll /t:module class1.vb
|
The result of either command will be a module file with the name class1.netmodule.
If you open class1.netmodule in ILDASM and drill down to the manifest, you will see that there are no references to itself as an assembly of the form:
.assembly class1
This verifies that you have created a module that is not yet an assembly. In comparison, if you were to compile the same source code as a library with the following command line:
csc /t:library /out:class1.dll class1.cs
then open class1.dll in ILDASM and look at the manifestyou will see the following section (with some lines truncated or omitted):
.assembly class1 { .hash algorithm 0x00008004 .ver 0:0:0:0 }
which indicates that the DLL is a self-contained assembly.
Drilling down through Class1, you will see the static method (Shared in VB.NET) SayHi, which is present in Class1.cs or Class1.vb.
Next, compile Form1.cs or Form1.vb into a module. The command for doing so is similar to the previous command, with the addition of an /addmodule: argument that brings class1.netmodule into the picture. Use this command:
csc /addmodule:class1.netmodule /t:module form1.cs
vbc /r:system.dll, system.windows.forms.dll,system.drawing.dll /addmodule:class1.netmodule /t:module form1.vb
|
If you wanted to refer to a pre-existing PE file, such as a DLL or EXE, you would use the /reference: argument. However, class1.netmodule is not a PE file. The /addmodule: argument performs the same function for a module. It is necessary here because form1.cs/form1.vb makes calls to a method contained in class1.
Again, the output from this command in either language will be a module file called form1.netmodule.
22.3.4.2.3 Linking the modules into an assembly
The modules created so far can be linked into an assembly using the Assembly Linker tool, al.exe. This command has the generic form:
al ... /main: /out: /target:
The module names constitute the source modules that will be linked to the assembly. You can also embed or link resource files as additional source files. The /main: argument specifies the fully qualified method that is the entry point when creating an executable file. The /out: argument specifies the name of the output file. It is the only argument that is actually required. The /target: argument (/t: for short) specifies the type of output file. Valid values are analogous to, but slightly different from, the target values used in the compilers: lib for library file (i.e., a DLL), exe for a console application, and win for a windows application.
For example, enter the following command line (in either language):
al class1.netmodule form1.netmodule /main:csDeploy.Form1.Main /out:Deploy.exe /target:win
|
The result is an executable called Deploy.exe, which will be a Windows application. Looking at the output file in ILDASM, you will see only a manifest and a static method called _EntryPoint. The actual IL code comprising the application is contained in the two .netmodule files. Looking into the manifest, you will see, among other things, the following lines:
.module extern Form1.netmodule .assembly Deploy .file Class1.netmodule .file Form1.netmodule .class extern public csDeployLibrary.Class1 .class extern public csDeploy.Form1
From these lines, you can see that this assembly references an external file called Form1.netmodule (which itself references Class1.netmodule), Deploy is itself an assembly, the two files Class1.netmodule and Form1.netmodule are required, and the two external classes Class1 and Form1 are referenced. To deploy this application, you must deploy three files: Deploy.exe, Class1.netmodule, and Form1.netmodule.
|
22.3.5 Versioning
Every assembly can have a four-part version number assigned to it, of the form:
...
Each part of the version can have any meaning you wish to assign. There is no enforced meaning to the first number as opposed to the second, for example. The generally recommended meanings are that the major version represents a distinctly new release that may not be backward compatible with previous versions, the minor version represents a significant feature enhancement that probably is backward compatible, the build number represents a bug fix or patch level, and the revision number represents a specific compilation. Your marketing department may have other ideas, and you can assign any versioning meaning you wish to the parts of the version number.
|
In Figure 22-4, every assembly has a version associated with it. For example, System.Windows.Forms has the following version attribute:
.ver 1:0:3300:0
corresponding to a major version of 1, a minor version of 0, a build number of 3300, and a revision number of 0.
Version numbers are part of the assembly's identity. The CLR considers two assemblies that differ only in version number as two distinctly different assemblies. This consideration allows multiple versions of the same assembly to reside side by side in the same application, and even on the same machine.
|
As you will see shortly, the CLR differentiates between two different types of assemblies: private and shared. The CLR ignores the version number of private assemblies. Adding a version number to a private assembly is for self-documentation (only for the benefit of people perusing the source code or the manifest). However, if an assembly is shared (explained in Section 22.3.6), then the CLR is cognizant of the version and can use it to allow or disallow the assembly to load, depending on which version is needed.
Versions are assigned to an assembly with assembly attributes, either at the top of your main source file or at the top of a separate source file compiled into the assembly.
|
Any source file that will include attributes must make reference to the System.Reflection namespace (unless you type in fully qualified attribute names). Include the following using statement:
using System.Reflection;
imports System.Reflection
The attribute, or attributes, must be at the top of the source file, after the using/imports statements, but before any class definitions. It looks something like the following:
[assembly: AssemblyVersion("1.1.*")]
|
The argument provided to the attribute is a string. Although the four parts of the version number have the meanings just described (major, minor, build, and revision), you can use any values you want. To the extent that the CLR checks the version number, it does not enforce any meaning, but compares whether the total version number is equal to, greater than, or less than a specified value, or whether it falls within a specified range.
That said, the Framework does impose some rules, and it provides shortcuts for automatically generating meaningful version numbers.
Visual Studio .NET defaults the version number in AssemblyInfo.cs/.vb with the following line of code:
[assembly: AssemblyVersion("1.0.*")]
and this line in AssemblyInfo.vb:
22.3.6 Private Versus Shared Assemblies
Broadly speaking, there are two types of assemblies: private and shared. A private assembly is used only by a single application, while a shared assembly can be used by more than one application.
A private assembly is located in the application directory (the same directory as the program executable and all the other private assemblies associated with the application). This directory is also known as the AppBase, or the application base directory.
Any public member (method, field, or property) contained in a private assembly will be available to any application in that directory, just by virtue of its presence in the directory. There is no need to register the assembly with the Registry, for example, as is the case with COM.
Private assemblies make no provision for versioning. The CLR does not check the version of private assemblies and cannot make load decisions based on version number. From this, it follows that it is not possible to have multiple versions of the same assembly in the same directory. However, different directories can each have their own copy of a given assembly, which may or may not include different versions. The intent here is to allow different applications to have different versions of the same assembly.
COM allows only a single copy of a given DLL on a machine, to be used by all applications requiring that DLL. (Actually, support for side-by-side COM DLL's has been added to Windows XP, but this is a relatively new feature.) When hard disk space was a precious commodity, single copies of each DLL was a laudable, if imperfectly implemented, goal. Now, with very large hard drives making disk space a virtually unlimited resource, it makes sense to allow multiple copies of DLL's, one for each application that needs it. The benefit of this approach is the elimination of DLL Hell, as well as vastly simplified installation and management. The deployment ramifications of private assemblies are discussed later in this chapter in Section 22.5.1.
|
In contrast, a shared assembly can be made available to multiple applications on the machine. Typically, (although this is not a requirement; you can specify an alternative location with a element in a configuration file, as described shortly) shared assemblies are located in a special area of the drive called the Global Assembly Cache (GAC). The GAC will be discussed in more detail shortly.
|
Shared assemblies also eliminate DLL Hell because the version of the assembly is part of its identity. An application will either use the version of the assembly it was originally compiled with or the version specified by the version policy contained in a controlling configuration file.
|
Shared assemblies in the GAC offer some benefits over shared assemblies not in the GAC, and shared assemblies generally offer several benefits over private assemblies, although they are much more of a bother to prepare, install, and administer. These benefits include:
22.3.7 Strong Names
For an assembly to be shared, it must have a strong name. A strong name uniquely identifies a particular assembly. It is comprised of a concatenation of:
A typical fully qualified name might look like:
myAssembly,Version=1.0.0.1,Culture=en-US,PublicKeyToken=9e9ddef18d355781
A strong name with all four parts is fully qualified, while one with fewer than all four components is partially qualified. If the culture is omitted, it can be specified as neutral. If the public key is omitted, it can be specified as null.
The public key identifies the developer or organization responsible for the assembly. Functionally, it replaces the role of GUIDs in COM, guaranteeing that the name is unique. It is the public half of a public key encryption scheme. The token listed as part of the strong name is a hash of the public key.
A public key encryption scheme, also called asymmetric encryption, relies on two numbers: a public key and a private key. They are mathematically related in such a way that if one key is used to encrypt a message, that message can be decrypted only by the other key, and vice versa. Furthermore, it is computationally infeasible, although not totally impossible, to determine one key, given only the other. (Given enough time with a supercomputer, any encryption scheme can be broken.)
Many algorithms are available for calculating hashes. The only two supported by the .NET Framework at this time are the MD5 and SHA1 algorithms. The algorithm used for an assembly is indicated in the manifest by the keywords .hash algorithm, followed by 0x00008003 for MD5 or 0x00008004 for SHA1.
The general principle is this: you generate a pair of keys, designating one as private and one as public. Keep your private key safe and secret.
A hash code is generated for the assembly using the specified encryption algorithm, typically SHA1. That hash code is then encrypted using RSA encryption and the private key. The encrypted hash code is embedded in the assembly manifest along with the public key. (The spaces where the encrypted hash code and the public key go in the manifest are set to zeros before the encryption and taken into account by the encryption program.)
The CLR decrypts the hash code included in the manifest using the public key. The CLR also uses the algorithm indicated in the manifest, again typically SHA1, to hash the assembly. The decrypted hash code is compared to the just-generated hash code. If they match, the CLR can be sure that the assembly has not been altered since it was signed.
22.3.7.1 Creating a strong name, step by step
Two steps are required to generate a strong name for an assembly.
The first step creates the public/private pair of keys. The .NET Framework provides a command-line tool for this purpose, sn.exe. Generate a pair of keys by executing sn with the -k option and the name of the file to hold the keys.
sn -k KeyPair.snk
|
Save this file and guard it carefully if you are going to use the keys for providing proof of origin. Make a copy (on diskette or CD) and put it in a secure place, such as a safe deposit box. (If you are just using the keys for testing or as a guaranteed unique identifier, then there is no need for this level of paranoia.) This file contains the private key that you should use for all assemblies created by your organization.
In a large organization where it is not feasible for all the developers to access the private key, you can use a procedure known as delayed signing, explained in the next section.
The second step is to either compile the source code, including the key file, into an assembly, or compile the source code into a module and then link that module (or modules) into an assembly along with the key file.
The most common way to include the key file as part of the compile is to use an AssemblyKeyFile attribute inserted into the source code, similar to the way an attribute was used previously in this chapter to set the version number.
When working with C# in Visual Studio .NET, this attribute is present by default in AssemblyInfo.cs, although with an empty string where the key filename would go. Whether in AssemblyInfo.cs or in another source file, the following line specifies the location of the key file:
[assembly: AssemblyKeyFile("c:\projects\KeyPair.snk")]
In VB.NET, the AssemblyKeyFile attribute is not included by default in AssemblyFile.vb. Add the following line to that file or to the top your source code file if working outside Visual Studio .NET:
The specified filename should have a fully qualified path so that you can use the same key file for all your projects. If the file does not contain a path, it assumes the current directory.
You can also specify a key file if compiling from the command line in VB.NET (but not in C#) with the /keyfile: option (/keyf: for short). For example:
vbc /out:myAssembly.dll /keyfile:keyfile.snk /target:library myAssembly.vb
Finally, you can compile modules without specifying a key file, and then include the key file as part of the linking process using the /keyfile: option (/keyf: for short). For example:
al myDLL.netmodule /keyfile:"c:projectsKeyPair.snk" /out:myDLL.dll /target:library
If a fully qualified path is not provided for the key file in either the VB.NET compiler or the Assembly Linker, the current directory will be assumed.
|
22.3.7.2 Delayed signing
As mentioned earlier, it is imperative that the private key be a closely guarded item. However, this presents a quandary: access to the private key is necessary to create a strong name for an assembly. Creating a strong name is necessary to properly develop and test a shared assembly. However, it may not be prudent to provide the firm's private key to all developers working on the project who legitimately need to create strong names.
To get around this quandary, use delayed signing, sometimes called partial signing. In this scenario, you create the strong-named assembly using only the public key, which is safe to disseminate to anybody who wants it. Do all your development and testing. Then when you are ready to do the final build, sign it properly with both the private and public keys.
The first step in delayed signing extracts the public key from the key file that contains both the private and public key. This is done from the command line using the sn tool again, passing it the -p option, the name of the key file, and the name of a file that holds the public key. In the following command, only the public key is contained in PublicKey.snk.
sn -p KeyPair.snk PublicKey.snk
If linking from the command line, link the assembly as you normally would, except using the public key file, PublicKey.snk, instead of the private/public key file, KeyPair.snk. In addition, add the /delaysign+ option.
al class1.netmodule /out:MyAssembly.dll /keyfile:PublicKey.snk /delaysign+
The more common way of implementing delayed signing is to add the AssemblyDelaySign attribute to your source code (typically in AssemblyInfo.cs/vb if working in Visual Studio .NET), giving it a value of true. Use the following lines of code:
[assembly: AssemblyKeyFile("c:\projects\PublicKey.snk")] [assembly: AssemblyDelaySign(true)]
In any case, before you can work further with the assembly, you must disable signature verification, again using the sn tool.
sn -Vr MyAssembly.dll
After your development, testing, and debugging is complete and you are ready to ship the DLL, sign the assembly with the correct private key. This step is usually performed by someone other than the normal developersomeone with access to the private key.
sn -R MyAssembly.dll KeyPair.snk
The final step is to then re-enable signature verification.
sn -Vu MyAssembly.dll
One additional useful option of the sn tool is -v, which allows you to verify the assembly, as in:
sn -vf MyAssembly.dll
This option will tell you whether the assembly is valid.
22.3.8 Global Assembly Cache (GAC)
The Global Assembly Cache (GAC) stores assemblies that need to be available to multiple applications on a machine (i.e., shared assemblies). The GAC is a special area of the directory structure, accessible only to administrators and typically located at c:windowsassemblyGAC.
The contents of the GAC can be seen and manipulated through either a command-line utility, GacUtil.exe (described shortly) or a special extension in Windows Explorer, shown in Figure 22-9.
Figure 22-9. Global Assembly Cache in Windows Explorer
|
Assuming that an assembly has a strong name, you can add it to the GAC by dragging it from one Explorer window into the GAC Explorer window. Likewise, you can delete assemblies from the GAC in Windows Explorer by selecting it and pressing Delete or by right-clicking and selecting Delete.
Right-clicking on an assembly and selecting Properties brings up the Properties dialog box shown in Figure 22-10 for System.dll.
Figure 22-10. System.dll properties in GAC
|
22.3.8.1 GacUtil.exe
Assemblies can be added and removed from the GAC by using Windows Explorer, as just described. A command-line utility called GacUtil.exe allows the same functionality.
To add an assembly to the GAC with GacUtil, use the /i option.
gacutil /i myAssembly.dll
To remove an assembly, use the /u option.
gacutil /u myAssembly
Note that when installing files, the file extension must be included, but when removing the assembly, no extension is used.
GacUtil can remove multiple assemblies with one command. This can be a strong point, if it is your intent, but it can be a disaster if it is not. Suppose you have two different versions of myAssembly in the GAC, Version 1.1.0.0 and 1.2.0.0. The command line shown above would delete both versions.
You can specify either a fully or partially qualified strong name to specifically identify the assembly to be removed. So to remove only Version 1.1.0.0, you could use either of the following two commands. The first uses a partially qualified strong name consisting of only the text name and the version, and the second uses the fully qualified strong name.
gacutil /u myAssembly,version=1.1.0.0 gacutil /u myAssembly,version=1.1.0.0,culture=neutral,PublicKeyToken=9e9ddef18d355781
|
You can list the contents of the GAC using the /l option.
gacutil /l
Alternatively, you can list a specific assembly by including its name.
gacutil /l myAssembly
22.3.9 Resolving and Binding Assemblies
When the CLR receives a reference to an object, the assembly containing the object is either already present in memory and bound to the calling application or it must be loaded. Resolving an assembly is the process of figuring out whether it needs to be loaded into memory, and if so, which version and from what location. Once an assembly is resolved, the calling application can bind to it.
References to an assembly can be either static or dynamic. Static references are hardcoded into an assembly manifest. Dynamic references are constructed at runtime by calling methods such as those from the System.Reflection namespace. For example, a call to the static (Shared in VB.NET) method System.Reflection.Assembly.Load returns an instance of an assembly.
Regardless of whether it is static or dynamic, the CLR goes through the same steps for resolving the assembly reference if it has a fully qualified strong name. These steps are described below. If only the simple name is made available to the calling method, then only the application directory is searched for the assembly, and none of the other steps are performed to try to locate the assembly.
For example, the following line of code loads Version 1.2.10.507 of myNamespace.myAssembly with the French culture and the specified public key token.
System.Reflection.Assembly.Load("myNamespace.myAssembly, Culture=fr, PublicKeyToken=a5d015c7d5a0b012, Version=1.2.10.507")
The CLR uses the steps described below to locate this specific assembly. On the other hand, this line of code:
System.Reflection.Assembly.Load("myNamespace.myAssembly")
causes the CLR to look in the application directory and no further.
|
The CLR takes the following steps to resolve a strongly named assembly:
The CLR attempts to bind to the same version of an assembly as the calling application was built with, unless this default behavior is overridden by configuration files imposing a different version policy.
The CLR first looks at the version policy specified machine-wide in machine.config, then at the policy specified in the publisher configuration file (unless the application configuration file specifically says to ignore publisher policy), and then finally in the application configuration file. Each successive configuration file builds on the settings of the previous one.
Each configuration file can have an element that in turn contains one or more sections, one for each assembly that requires specification.
Within each section, there is:
If a element is used and the assembly is not found at the specified location, an exception is raised and the search stops.
Example 22-18. Sample version policy in configuration file
Windows Forms and the .NET Framework
Getting Started
Visual Studio .NET
Events
Windows Forms
Dialog Boxes
Controls: The Base Class
Mouse Interaction
Text and Fonts
Drawing and GDI+
Labels and Buttons
Text Controls
Other Basic Controls
TreeView and ListView
List Controls
Date and Time Controls
Custom Controls
Menus and Bars
ADO.NET
Updating ADO.NET
Exceptions and Debugging
Configuration and Deployment