The basic unit of .NET deployment is the assembly . An assembly is a collection of files that appear to be a single DLL or executable (EXE). As noted earlier, DLLs are collections of classes and methods that are linked into your running program only when they are needed.
Assemblies are the .NET unit of reuse, versioning, security, and deployment. This chapter discusses assemblies in detail, including the architecture and contents of assemblies, private assemblies, and shared assemblies.
In addition to the object code for the application, assemblies contain resources such as .gif files, type definitions for each class you define, as well as other metadata about the code and data.
On disk, assemblies are Portable Executable (PE) files. PE files aren’t new. The format of a .NET PE file is exactly the same as a normal Windows PE file. PE files are implemented as DLLs or EXEs.
Physically, assemblies consist of one or more modules . Modules are the constituent pieces of assemblies. Standing alone, modules can’t be executed; they must be combined into assemblies to be useful.
You will deploy and reuse the entire contents of an assembly as a unit. Assemblies are loaded on demand, and will not be loaded if not needed.
Metadata is information stored in the assembly that describes the types and methods of the assembly and provides other useful information about the assembly. Assemblies are said to be self-describing because the metadata fully describes the contents of each module. Metadata is discussed in detail in Chapter 18.
Assemblies form security boundaries as well as type boundaries. That is, an assembly is the scope boundary for the types it contains, and type definitions can’t cross assemblies. You can, of course, refer to types across assembly boundaries by adding a reference to the required assembly, either in the IDE or on the command line, at compile time. What you can’t do is have the definition of a type span two assemblies.
The internal access modifier limits access (for a method, for example) to the current assembly.
As part of its metadata, every assembly has a manifest . This describes what is in the assembly: identification information (name, version, etc.), a list of the types and resources in the assembly, a list of modules, a map to connect public types with the implementing code, and a list of assemblies referenced by this assembly.
Even the simplest program has a manifest. You can examine that
manifest using ILDasm
, which is provided as part
of your development environment. When you open the manifest in
ILDasm, the EXE
program created by Example 12-3 looks like Figure 17-1.
Notice the manifest (second line from the top). Double-clicking the manifest opens a Manifest window, as shown in Figure 17-2.
This file serves as a map of the contents of the assembly. You can
see in the first line the reference to
the
mscorlib
assembly, which is referenced by this and
every .NET application. The mscorlib
assembly is
the core library assembly for .NET and is available on every .NET
platform.
The next assembly line is a reference to the assembly from Example 12-3. You can also see that this assembly consists of a single module. You can ignore the rest of the metadata for now.
Assemblies can consist of more than one module, though this isn’t supported by Visual Studio 2005.
A single-module assembly has a single file that can be an EXE or DLL file. This single module contains all the types and implementations for the application. The assembly manifest is embedded within this module.
Each module has a manifest of its own that is separate from the assembly manifest. The module manifest lists the assemblies referenced by that particular module. In addition, if the module declares any types, these are listed in the manifest along with the code to implement the module. A module can also contain resources, such as the images needed by that module.
A multimodule assembly consists of multiple files (zero or one EXE and zero or more DLL files, though you must have at least one EXE or DLL). The assembly manifest in this case can reside in a standalone file, or it can be embedded in one of the modules. When the assembly is referenced, the runtime loads the file containing the manifest and then loads the required modules as needed.
To
demonstrate
the use of multimodule assemblies, the following example creates a
couple of very simple modules that you can then combine into a single
assembly. The first module is a Fraction
class.
This simple class will allow you to create and manipulate common
fractions. Example 17-1 illustrates.
Example 17-1. The Fraction class
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ProgCS { public classFraction { private int numerator; private int denominator; public Fraction( int numerator, int denominator ) { this.numerator = numerator; this.denominator = denominator; } public Fraction Add( Fraction rhs ) { if ( rhs.denominator != this.denominator ) { return new Fraction( rhs.denominator * numerator + rhs.numerator * denominator, denominator * rhs.denominator); } return new Fraction( this.numerator + rhs.numerator, this.denominator ); } public override string ToString( ) { return numerator + "/" + denominator; } } }
Notice that the Fraction
class is in the
ProgCS
namespace. The full name for the
class is ProgCS.Fraction
.
The Fraction
class takes two values in its
constructor: a numerator
and a
denominator
. There is also an
Add( )
method, which takes a second
Fraction
and returns the sum, assuming the two
share a common denominator
. This class is
simplistic, but it will demonstrate the functionality necessary for
this example.
The second class is the MyCalc
class, which stands
in for a robust calculator. Example 17-2 illustrates.
Example 17-2. The calculator
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
#endregion
namespace ProgCS
{
public classMyCalc
{
public int Add( int val1, int val2 )
{
return val1 + val2;
}
public int Mult( int val1, int val2 )
{
return val1 * val2;
}
}
}
Once again, MyCalc
is a very stripped-down class to keep things simple.
Notice that MyCalc
is also in the
ProgCS
namespace.
This is sufficient to create an assembly. Use an AssemblyInfo.cs file to add some metadata to the assembly. The use of metadata is covered in Chapter 18.
You can write your own AssemblyInfo.cs file, but the simplest approach is to let Visual Studio generate one for you automatically.
Visual Studio creates only single-module assemblies.
You can create a multimodule resource with the
/addModules
command-line option. The easiest way
to compile and build a multimodule assembly is with
a makefile, which you can create with
Notepad or any text editor.
If you are unfamiliar with makefiles, don’t worry; this is the only example that needs a makefile, and that is just to get around the current limitation of Visual Studio creating only single-module assemblies. If necessary, you can just use the makefile as offered without fully understanding every line. For more information, see Managing Projects with make (O’Reilly).
Example 17-3 shows the complete makefile (which is
explained in detail immediately afterward). To run this example, put
the makefile (with the name makefile) in a
directory together with a copy of Calc.cs,
Fraction.cs, and
AssemblyInfo.cs. Start up a .NET command window
and cd
to that directory. Invoke
nmake without any command switch. You will find
the SharedAssembly.dll in the
in subdirectory.
Example 17-3. The complete makefile for a multimodule assembly
ASSEMBLY= MySharedAssembly.dll BIN=.in SRC=. DEST=.in CSC=csc /nologo /debug+ /d:DEBUG /d:TRACE MODULETARGET=/t:module LIBTARGET=/t:library EXETARGET=/t:exe REFERENCES=System.dll MODULES=$(DEST)Fraction.dll $(DEST)Calc.dll METADATA=$(SRC)AssemblyInfo.cs all: $(DEST)MySharedAssembly.dll # Assembly metadata placed in same module as manifest $(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s # Add Calc.dll module to this dependency list $(DEST)Calc.dll: Calc.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s # Add Fraction $(DEST)Fraction.dll: Fraction.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s $(DEST):: !if !EXISTS($(DEST)) mkdir $(DEST) !endif
The makefile begins by defining the assembly you want to build:
ASSEMBLY= MySharedAssembly.dll
It then defines the directories you’ll use, putting the output in a bin directory beneath the current directory and retrieving the source code from the current directory:
SRC=. DEST=.in
Build the assembly as follows:
$(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s
This places the assembly (MySharedAssembly.dll)
in the destination directory (bin). It tells
nmake
(the program that executes the
makefile) that the $(DEST)$(ASSEMBLY)
build
target depends upon the three other build targets listed, and it
provides the command line required to build the assembly.
The metadata is defined earlier as:
METADATA=$(SRC)AssemblyInfo.cs
The modules are defined as the two DLLs:
MODULES=$(DEST)Fraction.dll $(DEST)Calc.dll
The compile line builds the library and adds the modules, putting the output into the assembly file MySharedAssembly.dll:
$(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s
To accomplish this, nmake needs to know how to make the modules. Start by telling nmake how to create Calc.dll. You need the Calc.cs source file for this; tell nmake the command line to build that DLL:
$(DEST)Calc.dll: Calc.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
Then do the same thing for Fraction.dll:
$(DEST)Fraction.dll: Fraction.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
The result of running nmake on this makefile is to create three DLLs: Fraction.dll, Calc.dll, and MySharedAssembly.dll. If you open MySharedAssembly.dll with ILDasm, you’ll find that it consists of nothing but a manifest, as shown in Figure 17-3.
If you examine the manifest, you see the metadata for the libraries you created, as shown in Figure 17-4.
You first see an external assembly for the core library
(mscorlib
), followed by the two modules,
ProgCS.Fraction
and
ProgCS.myCalc
.
You now have an assembly that consists of three DLL files: MySharedAssembly.dll with the manifest, and Calc.dll and Fraction.dll with the types and implementation needed.
To use these modules, you’ll create a driver program. Example 17-4 illustrates. Save this program as Test.cs in the same directory as the other modules.
Example 17-4. A module test-driver
namespace Programming_CSharp { using System; public class Test { // main will not load the shared assembly static void Main( ) { Test t = new Test( ); t.UseCS( ); t.UseFraction( ); } // calling this loads the myCalc assembly // and the mySharedAssembly assembly as well public void UseCS( ) { ProgCS.myCalc calc = new ProgCS.myCalc( ); Console.WriteLine("3+5 = {0} 3*5 = {1}", calc.Add(3,5), calc.Mult(3,5)); } // calling this adds the Fraction assembly public void UseFraction( ) { ProgCS.Fraction frac1 = new ProgCS.Fraction(3,5); ProgCS.Fraction frac2 = new ProgCS.Fraction(1,5); ProgCS.Fraction frac3 = frac1.Add(frac2); Console.WriteLine("{0} + {1} = {2}", frac1, frac2, frac3); } } } Output: 3+5 = 8 3*5 = 15 3/5 + 1/5 = 4/5
For the purposes of this demonstration, it is important not to put
any code in Main( )
that depends on your modules.
You don’t want the modules loaded when
Main( )
loads, and so no
Fraction
or Calc
objects are
placed in Main(
)
. When you
call into UseFraction
and
UseCalc
, you’ll be able to see
that the modules are individually loaded.
An assembly is loaded into its
application by the
AssemblyResolver
through a process called
probing
. The assembly
resolver is called by the .NET Framework automatically; you
don’t call it explicitly. Its job is to load your
program.
The three DLLs produced earlier must be in the directory in which Example 17-4 executes or in a subdirectory of that directory that is in the binpath (the user-defined list of subdirectories under the root location that is specified in the application configuration file).
Put a breakpoint on the second line in Main()
, as
shown in Figure 17-5.
Execute to the breakpoint and open the Modules window. Only two of our modules are loaded, as shown in Figure 17-6.
If you didn’t
develop Test.cs
as part of a Visual Studio .NET solution, put a call to
System.Diagnostics.Debugger.Launch( )
just before
the second line in Main()
. This lets you choose
which debugger to use. (Make sure to compile
Test.cs with the options
/debug
and
/r:MySharedAssembly.dll
.)
Step into the first method call and watch the Modules window. As soon
as you step into UseCS
, the
AssemblyLoader
recognizes that it needs a module
from MySharedAssembly.dll. The DLL is loaded,
and from that assembly’s manifest the
AssemblyLoader
finds that it needs
Calc.dll, which is loaded as well, as shown in
Figure 17-7.
When you step into Fraction
, the final DLL is loaded.
Assemblies come in two flavors: private and shared . Private assemblies are intended to be used by only one application; shared assemblies are intended to be shared among many applications.
All the assemblies you’ve built so far are private. By default, when you compile a C# application, a private assembly is created. The files for a private assembly are all kept in the same folder (or in a tree of subfolders). This tree of folders is isolated from the rest of the system, as nothing other than the one application depends on it, and you can redeploy this application to another machine just by copying the folder and its subfolders.
A private assembly can have any name you choose. It doesn’t matter if that name clashes with assemblies in another application; the names are local only to a single application.
In the past, DLLs were installed on a machine and (for COM DLLs) an entry was made in the Windows Registry. It was difficult to avoid polluting the Registry with useless cruft. In any case, reinstalling the program on another machine was nontrivial. With assemblies, all of that goes away. With private assemblies, installing is as simple as copying the files to the appropriate directory (called xcopy deployment). Period.
You can create assemblies that can be shared by other applications. You might want to do this if you have written a generic control or a class that might be used by other developers. If you want to share your assembly, it must meet certain stringent requirements.
First, your assembly must have a strong name. Strong names are globally unique.
No one else can generate the same strong name as you because an assembly generated with one private key is guaranteed to have a different name than any assembly generated with another private key.
Second, your shared assembly must be protected against newer versions trampling over it, and so each new version you release must have a new version number.
Finally, to share your assembly, place it in the Global Assembly Cache (GAC) (pronounced “gak”). This is an area of the filesystem set aside by the CLR to hold shared assemblies.
Assemblies mark the end of DLL Hell. Remember this scenario: you install Application A on your machine, and it loads a number of DLLs into your Windows directory. It works great for months. You then install Application B on your machine, and suddenly, unexpectedly, Application A breaks. Application B is in no way related to Application A. So what happened? It turns out, you later learn, that Application B replaced a DLL that Application A needed, and suddenly Application A began to stagger about, blind and senseless.
When DLLs were invented, disk space was at a premium and reusing DLLs seemed like a good idea. The theory was that DLLs would be backward-compatible, so automatically upgrading to the new DLL would be painless and safe. As my old boss Pat Johnson used to say, “In theory, theory and practice are the same. But in practice, they never are.”
When the new DLL was added to the computer, the old application, which was happily minding its own business in another corner of your machine, suddenly linked to a DLL that was incompatible with its expectations and hey! Presto! It went into the dance of death. This phenomenon led customers to be justifiably leery of installing new software, or even of upgrading existing programs, and it is one of the reasons Windows machines are perceived to be unstable. With assemblies, this entire nightmare goes away.
Shared assemblies in .NET are uniquely identified by their names and their versions. The GAC allows for “side-by-side” versions in which an older version of an assembly is available alongside a newer version.
Side-by-side versioning applies only to items in the GAC. Private assemblies don’t need this feature and don’t have it.
A version number for an assembly might look like this:
1:0:2204:21
(four numbers, separated by colons).
The first two numbers (1:0
) are the major and
minor versions. The third number (2204
) is the
build, and the fourth (21
) is the revision.
When two assemblies have different major or minor numbers, they are considered by convention to be incompatible. When they have different build numbers, they might or might not be compatible, and when they have different revision numbers, they are considered definitely compatible with each other. This is great in theory, but the CLR assembly resolver ignores this convention and it serves only to remind the developer; it isn’t enforced at runtime.
To use a shared assembly, you must meet two requirements:
You need to be able to specify the exact assembly you want to load.
You need to ensure that the assembly has not been tampered with and that the assembly being loaded is the one authored by the actual creator of the assembly. To do so, your assembly needs a digital signature when it is built.
Both of these requirements are met by strong names. Strong names must be globally unique and use public key encryption. A strong name is a string of hexadecimal digits and isn’t meant to be human-readable.
To create a strong name, a public-private key pair is generated for one or more assemblies. A hash is taken of the names and contents of the files in the assembly. The hash is then encrypted with the private key for the assembly, and the public key token (an 8-byte hash of the full key) is placed in the manifest along with the public key. This is known as signing the assembly .
When an application loads the assembly, the CLR uses the public key to decode the hash of the files in the assembly to ensure that they have not been tampered with. This also protects against name clashes.
You can create a strong name with the sn
utility:
sn -k c:myStrongName.snk
The -k
flag indicates that you want a new key pair
written to the specified file. You can call the file anything you
like. Remember, a strong name is a string of bytes and
isn’t meant to be human-readable.
You can associate this strong name with your assembly by using an attribute:
using System.Runtime.CompilerServices; [assembly: AssemblyKeyFile("c:myStrongName.key")]
Attributes are covered in detail in Chapter 18. For now, you can just put this code at the top of your file to associate the strong name you generated with your assembly.
Once you’ve created your
strong name and associated it with your assembly, all that remains is
to place the assembly in the GAC. You can do so with the
gacutil
utility:
gacutil /i MySharedAssembly.dll
Or you can open your File Explorer and drag your assembly into the GAC. To see the GAC, open the File Explorer and navigate to %SystemRoot%assembly; Explorer turns into a GAC utility.
The best way to understand shared assemblies is to build one. Let’s return to the earlier multimodule project (see Examples Example 17-1 through Example 17-4) and navigate to the directory that contains the files Calc.cs and Fraction.cs.
Try this experiment: locate the bin directory for the driver program and make sure that you don’t have a local copy of the MySharedAssembly DLL files.
Run the program. It should fail with an exception saying it can’t load the assembly:
Unhandled Exception: System.IO.FileNotFoundException: File or assembly name MySharedAssembly, or one of its dependencies, was not found. File name: "MySharedAssembly" at Programming_CSharp.Test.UseCS( ) at Programming_CSharp.Test.Main( )
Now copy the DLLs into the driver program’s directory tree, run it again, and this time you should find that it works fine.
Let’s make the MySharedAssembly
into a shared assembly. This is done in two steps. First, create a
strong name for the assembly, and then put the assembly into the GAC
(of course, you are also free to just use this strongly named
assembly via xcopy deployment if you choose).
Create a key pair by opening a command window and entering:
sn -k keyFile.snk
Now open the AssemblyInfo.cs file in the project for the MySharedAssembly.dll and modify this line:
[assembly: AssemblyKeyFile("")]
as follows:
[assembly: AssemblyKeyFile("keyFile.snk")]
This sets the key file for the assembly. Rebuild with the same makefile as earlier, and then open the resulting DLL in ILDasm and open the manifest. You should see a public key, as shown in Figure 17-8.
By adding the strong name, you have signed this assembly (your exact values will be different). To illustrate that the names match in the GAC and in the reference in the client manifest, you’ll want to get the strong name from the DLL. To do this, navigate to the directory with the DLL and enter the following at a command prompt:
sn -T MySharedAssembly.dll
The response should be something like this:
Public key token is 01fad8e0f0941a4d
This value is an abbreviated version of the assembly’s public key, called the public key token .
Remove the DLLs from the test program’s directory structure and run it again. It should fail again. Although you’ve given this assembly a strong name, you haven’t yet registered it in the GAC.
The next step is to drag the library into the GAC. To do so, open an Explorer window and navigate to the %SystemRoot% directory. When you double-click the Assembly subdirectory, Explorer turns into a GAC viewer.
You can drag and drop into the GAC viewer, or you can invoke this command-line utility:
Gacutil /i mySharedAssembly.dll
Just to close the circle, you might want to check that your assembly
was loaded into the GAC, and that the public key token shown in the
GAC viewer matches the value you got back from sn
:
Public key token is 01fad8e0f0941a4d
This is illustrated in Figure 17-9.
Once this is done, you have a shared assembly that can be accessed by any client. Refresh the client by building it again, and look at its manifest, as shown in Figure 17-10.
There’s MySharedAssembly
, listed
as an external assembly, and the public key now matches the value
shown in the GAC. Very nice; time to try it.
Close ILDasm, and run your code. It should work fine, even though there are no DLLs for this library in its immediate path. You’ve just created and used a shared assembly.
The assembly manifest also contains references to other assemblies. Each such reference includes the name of the other assembly, the version number and required culture, and optionally, the other assembly’s public key token (a digital signature).
Culture is a string representing the language and national display characteristics for the person using your program. It is culture that determines, for example, whether dates are in month/date/year format or date/month/year format.
3.14.252.56