Chapter 17. Assemblies and Versioning

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.

PE Files

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

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.

Security Boundary

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.

Manifests

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.

ILDasm of Example 12-3

Figure 17-1. ILDasm of Example 12-3

Notice the manifest (second line from the top). Double-clicking the manifest opens a Manifest window, as shown in Figure 17-2.

The Manifest window

Figure 17-2. The Manifest window

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.

Multimodule Assemblies

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.

Building a Multimodule Assembly

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.

Tip

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.

Tip

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.

MySharedAssembly.dll

Figure 17-3. MySharedAssembly.dll

If you examine the manifest, you see the metadata for the libraries you created, as shown in Figure 17-4.

The manifest for MySharedAssembly.dll

Figure 17-4. The manifest for MySharedAssembly.dll

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.

Testing the assembly

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.

Loading the assembly

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.

Tip

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.

A breakpoint in Main( )

Figure 17-5. A breakpoint in Main( )

Execute to the breakpoint and open the Modules window. Only two of our modules are loaded, as shown in Figure 17-6.

Only two modules loaded

Figure 17-6. Only two modules loaded

Tip

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.

Modules loaded on demand

Figure 17-7. Modules loaded on demand

When you step into Fraction, the final DLL is loaded.

Private Assemblies

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.

Shared Assemblies

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.

Tip

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.

The End of DLL Hell

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.

Versions

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.

Tip

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.

Strong Names

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.

The Global Assembly Cache

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.

Building a Shared Assembly

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.

Tip

The referenced assembly (MySharedAssembly) should have its CopyLocal property set to false.

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

Step 1: Create a strong name

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.

The manifest of MySharedAssembly.dll

Figure 17-8. The manifest of MySharedAssembly.dll

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

Tip

Note that sn is case-sensitive. Don’t write sn -t.

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.

Step 2: Put the shared assembly 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.

The GAC

Figure 17-9. The GAC

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.

The manifest

Figure 17-10. The manifest

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.

Other Required Assemblies

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.174.23