Assemblies

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.

Although it is often useful to look at the IL code generated by a .NET compiler, IL code is beyond the scope of this book. For details on IL programming, we recommend CIL Programming: Under the Hood of .NET by Jason Bock (APress).

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

figs/pnwa_2203.gif

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.

Table 22-1. ILDASM icons

Icon

Description

figs/22icon_namespace.gif

Namespace (blue icon with red top edge)

figs/22icon_class.gif

Class (blue icon)

figs/22icon_interface.gif

Interface (blue icon with yellow letter I)

figs/22icon_valueclass.gif

Value class (yellow icon)

figs/22icon_enum.gif

Enum (yellow icon with purple letter E)

figs/22icon_method.gif

Method (pink icon)

figs/22icon_staticmethod.gif

Static method (pink icon with yellow letter S)

figs/22icon_field.gif

Field (aqua icon)

figs/22icon_staticfield.gif

Static field (aqua icon with dark blue letter S)

figs/22icon_event.gif

Event (green icon)

figs/22icon_property.gif

Property (red icon)

figs/22icon_manifest.gif

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.

Table 22-2. Adding ILDASM to Visual Studio .NET

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.

Assemblies created in Visual Studio .NET always contain only a single module, unless you are using Managed C++. To create multimodule assemblies in C# or VB.NET, you must compile from the command line. Multimodule assemblies will be discussed more fully in Section 22.3.4.

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

figs/pnwa_2204.gif

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:

figs/vbicon.gif

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

Multimodule assemblies can be created in Visual Studio .NET when using Managed C++.

A multimodule assembly might be desirable for several reasons:

  • To combine modules written in different languages into a single assembly
  • To combine code written by different developers into a single assembly
  • To package multiple modules that will always be used together and that must increment version number as a unit
  • To allow the assembly to be updated by updating only a few of the modules as well as the assembly manifest
  • To allow the CLR to load only the necessary modules, minimizing impact on resources

On the other hand, it is advantageous to have a single module assembly in other circumstances:

  • To provide versioning flexibility, since an assembly is the smallest deployable unit of a single version of code, although each module within a multimodule assembly can also have its own version assigned to it (see Section 22.3.5).
  • To optimize deployment performance when deploying the application over the LAN or WAN (see the Section 22.5).
  • For greater simplicity in deployment

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.

Since both C# and VB.NET examples are included, the projects here will be named csDeploy and vbDeploy, respectively. Likewise, the library projects will be named csDeployLibrary and vbDeployLibrary.

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#:

figs/csharpicon.gif

using System.Windows.Forms;

and in VB.NET:

figs/vbicon.gif

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.

figs/csharpicon.gif

public static void SayHi(string strMsg)
{
 MessageBox.Show(strMsg, "csConfigDeploy.Class1");
}

figs/vbicon.gif

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

figs/pnwa_2205.gif

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#

figs/csharpicon.gif

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#):

figs/csharpicon.gif

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

figs/pnwa_2206.gif

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)

figs/csharpicon.gif

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)

figs/vbicon.gif

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

figs/pnwa_2207.gif

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

figs/pnwa_2208.gif

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.

Though multimodule assemblies are rarely used, we've included this section for completeness.

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.

figs/csharpicon.gif

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

figs/vbicon.gif

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)

figs/vbicon.gif

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)

figs/vbicon.gif

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:

figs/csharpicon.gif

csc /t:module class1.cs

figs/vbicon.gif

vbc /r:system.windows.forms.dll /t:module class1.vb

The C# compiler does not need the references to Framework DLLs in the command-line compile because a file called csc.rsp contains "default" references for the C# compiler. There is no equivalent in VB.NET, so the references must be included in the command line.

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

csc /addmodule:class1.netmodule /t:module form1.cs

figs/vbicon.gif

vbc /r:system.dll,
system.windows.forms.dll,system.drawing.dll /addmodule:class1.netmodule /t:module form1.vb

Each command-line command shown here should be entered as a single line without pressing the Enter key until the end.

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 /main argument in al.exe is case sensitive for both C# and VB.NET. For instance, in the VB.NET example, /main:Form1.Main works, but /main:Form1.main does not.

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.

If you create an application requiring command-line building and linking, you will find it much easier to put all the commands into either a batch file or a make file.

 

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.

Although there is no enforced meaning to the four parts of the version number, the fact that they are ordered from most to least significant is used in the assembly-binding process if you specify a range of versions to redirect. Assembly binding is detailed later.

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.

Although it is possible to have side-by-side versions of the same assembly in the same application, this is rarely a good idea as a practical matter. You must go out of your way to make it happen.

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.

Visual Studio .NET automatically includes a file called AssemblyInfo.cs or AssemblyInfo.vb with every project. This file provides a convenient means of adding or modifying attributes.

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:

figs/csharpicon.gif

using System.Reflection;

figs/vbicon.gif

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:

figs/csharpicon.gif

[assembly: AssemblyVersion("1.1.*")]

figs/vbicon.gif


 

Version syntax in manifests uses colons to separate the numbers, while attributes in source code use periods.

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.

  • If you specify the version, you must specify at least the major revision numbere.g., "1" will result in Version 1.0.0.0.
  • You can specify all four parts of the version. If you specify fewer than four parts, the remaining parts will default to zeroe.g., "1.2" will result in Version 1.2.0.0.
  • You can specify the major and minor numbers, plus an asterisk for the build. This will result in the specified major and minor numbers, the build will be equal to the number of days since January 1, 2000, and the revision will be equal to the number of seconds since midnight local time, divided by 2 and truncated to the nearest integer. For example, "1.2.*" will result in Version 1.2.1138.28933 if the file was compiled on February 12, 2003 at 4:04:27 PM.
  • You can specify the major, minor, and build numbers, plus an asterisk for the revision. This will result in the specified major, minor, and build numbers, plus the revision will be equal to the number of seconds since midnight local time, divided by 2 and truncated to the nearest integer. For example, "1.2.3.*" will result in Version 1.2.3.28933 if the file was compiled at 4:04:27 PM.

Visual Studio .NET defaults the version number in AssemblyInfo.cs/.vb with the following line of code:

figs/vbicon.gif

[assembly: AssemblyVersion("1.0.*")]

and this line in AssemblyInfo.vb:

figs/vbicon.gif


 

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.

DLL Hell is a phenomenon in which the user installs a new program (A), and suddenly a different program (B) stops working. As far as the user is concerned, A has nothing to do with B, but unbeknownst to the user, both A and B share a DLL; unfortunately, they require different versions of that same DLL. This problem disappears with .NET: each application can have its own private version of the DLL, or the application specifies which version of the DLL it requires.

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.

There are often reasons for creating a shared assembly other than to share an assembly between applications. For example, to take advantage of Code Access Security (CAS), an assembly must have a strong name (described in the next section), which effectively makes it shared.

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.

Of course, nothing prevents a developer from releasing a new assembly with the same version numbers as a previous release. In this circumstance, you will have replaced DLL Hell with Assembly Hell.

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:

  • Performance

    • The CLR first looks for an assembly in the GAC, and then in the application's directory.
    • Assemblies stored in the GAC do not need to have their public key signature verified every time they are loaded, while shared assemblies not in the GAC do. However, private assemblies never have their signature verified because they do not have a strong name (described in the next section).
    • The files in a shared assembly in the GAC are verified to be present and neither tampered with nor corrupt when the assembly is installed in the GAC. For shared assemblies not in the GAC, this verification step is performed every time the assembly is loaded.
  • Versioning

    • Side-by-side execution, in which an application, or different applications, can use different versions of the same assembly. Remember that private assemblies in different application directories can also be different versions.
    • An application uses the same version of an assembly that it was originally compiled with unless overridden by a binding policy specified in a configuration policy.
    • Applications can be redirected to use a different version of an assembly (allowing easy updating).
  • Robustness

    • Files cannot be deleted except by an administrator.
    • All files in a shared assembly are verified to be present and neither tampered with nor corrupted.
    • The shared assembly itself, whether in the GAC or another location, is signed with a public key to ensure that it was not tampered with.

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:

  • The text name of the assembly (without any file extension)
  • The version
  • The culture
  • A public key token

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

The options passed to sn.exe are case sensitive.

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:

figs/csharpicon.gif

[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:

figs/vbicon.gif


 

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:

figs/vbicon.gif

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:

figs/vbicon.gif

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.

Some software development shops choose not to store the private/public key file on disk, deeming it too great a risk. Instead, they store the key pair in a hardware device. In this case, everything is done the same way as with a normal key file, except you substitute the /keyname option for the /keyfile option in the command-line tools, and substitute the AssemblyKeyName attribute for the AssemblyKeyFile attribute in your source code, where the keyname value refers to the name of the device.

 

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:

figs/csharpicon.gif

[assembly: AssemblyKeyFile("c:\projects\PublicKey.snk")]
[assembly: AssemblyDelaySign(true)]

figs/vbicon.gif


 

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

figs/pnwa_2209.gif

Although the contents of the GAC are displayed in Windows Explorer as "normal" files, they are in fact each physically present as a directory.

You can verify this by opening a command window and navigating to c:windowsassemblyGAC. Executing a dir command will show one subdirectory for each assembly contained in the GAC. Drilling further down into one of those subdirectories with the cd command will reveal another subdirectory, this one named after a concatenation of the version, an empty string representing a neutral culture (the culture would go between the two underscores), and the assembly's public key token.

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

figs/pnwa_2210.gif

The CodeBase property is the physical location where the assembly came from. For assemblies you create yourself, it would have a value such as file:///c:/projects/MyApp/MyDll.dll.

You will notice that some entries in Figure 22-9 are of type Native Image. These are assemblies that were pre-JITed into machine code, rather than IL code, using the Ngen.exe command-line tool. Native image assemblies may load and execute faster than IL assemblies, although benchmarking is needed in specific cases to verify this. These native image files are contained within the native image cache. For more information about Ngen.exe, refer to the SDK documentation.

 

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

The strong name provided to GacUtil must contain no spaces.

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.

If you want to use a partially qualified name and still have the CLR look in the GAC, then use the System.Reflection.Assembly.LoadWithPartialName method. Alternatively, use the Load method in conjunction with the tag in the application configuration file. This allows you to provide the full reference information in a config file rather hardcoded in the application.

The CLR takes the following steps to resolve a strongly named assembly:

  1. Use the version policy in the configuration files to determine the correct version to load. A sample version policy is shown in Example 22-18.

    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:

    • One section that specifies each available segment of the strong name
    • One or more sections, which specify which old version, or range of versions, of the assembly should be redirected to use which new version
    • A section that allows publisher policy configuration to be disabled for this assembly
    • A section, described shortly
  2. Check to see whether the assembly is already loaded in memory. If it is, that copy is used and no further steps are taken. If the assembly was originally called by another application, then that same copy in memory can be bound to the current application, as with any other Win32 DLL, if it is the correct version and the other parts of the strong name correspond.
  3. Look in the GAC for the assembly. If the assembly is found in the GAC, it is used and no further steps are taken.
  4. Probe the directory structure using the following substeps:

    1. Look at the location specified by the codebase hint. Codebase hints are contained in elements in the configuration files, which specify the URL (which may refer to a location on the Web or to a file on the local or network filesystem) where a specific version of the assembly can be found. A element is the only way to share an assembly other than installing it to the GAC. A strong name is required in either case.

      If a element is used and the assembly is not found at the specified location, an exception is raised and the search stops.

    2. If there is no matching element in any configuration file, then it uses the directories specified in the attribute of the element of the configuration file to build the tree of subdirectories to search. The directories specified are all subdirectories of the application directory, or AppBase. Only subdirectories specified in the attribute are searched, not the entire subdirectory tree of the AppBase. Both DLLs and EXEs are searched for until the assembly is found. Cultures specified as part of the strong name are taken into account when building the search tree.
  5. If it still is not found, then the Windows Installer is run, requesting the missing assembly, acting as an install-on-demand feature. (The Windows Installer will be covered in a subsequent section.)

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



Programming. NET Windows Applications
Programming .Net Windows Applications
ISBN: 0596003218
EAN: 2147483647
Year: 2003
Pages: 148

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net