Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#
Article

C# and the .NET Platform - Chapter 6

Rate me:
Please Sign up or sign in to vote.
4.69/5 (9 votes)
23 Nov 200165 min read 158.6K   82   3
Assemblies, Threads, and AppDomains
Sample Image - 1893115593_6.gif
Author Andrew Troelsen
Title C# and the .NET Platform
PublisherApress
ISBN 1893115593
Price US 59.95
Pages 1004

Assemblies, Threads, and AppDomains

Each of the applications developed during the first five chapters are along the lines of traditional 'stand alone' applications, given that all programming logic was contained within a single (EXE) binary. One aspect of the .NET lifestyle is the notion of binary reuse. Like COM, .NET provides the ability to access types between binaries in a language-independent manner. However, the .NET platform provides far greater language integration than classic COM. For example, the .NET platform supports cross-language inheritance (imagine a Visual Basic.NET class deriving from a C# class). To understand how this is achieved requires a deeper understanding of assemblies.

Once you understand the logical and physical layout of an assembly (and the related manifest), you then learn to distinguish between 'private' and 'shared' assemblies. You also examine exactly how the .NET runtime resolves the location of an assembly and come to understand the role of the Global Assembly Cache (GAC). Closely related to location resolution is the notion of application configuration files. As you will see, the .NET runtime can read the XML-based data contained within this file to bind to a specific version of a shared assembly (among other things).

This chapter wraps up with an examination of building multithreaded assemblies, using the types defined within the System.Threading namespace. If you are coming from a Win32 background, you will be pleased to see how nicely thread manipulation has cleaned up under the .NET framework.

Problems with Classic COM Binaries

Binary reuse (i.e., portable code libraries) is not a new idea. To date, the most popular way in which a programmer can share types between binaries (and in some respects, across languages) is to build what can now be regarded as 'classic COM servers.' Although the construction and use of COM binaries is a well-established industry standard, these little blobs have caused each of us a fair share of headaches. Beyond the fact that COM demands a good deal of complex infrastructure (IDL, class factories, scripting support, and so forth), I am sure you have also pondered the following related questions:

  • Why is it so difficult to version my COM binary? 
  • Why is it so complex to distribute my COM binary?

The .NET framework greatly improves on the current state of affairs and addresses the versioning and deployment problems head-on using a new binary format termed an assembly. However, before you come to understand how the assembly offers a clean solution to these issues, let's spend some time recapping the problems in a bit more detail.

Problem: COM Versioning

In COM, you build entities named coclasses that are little more than a custom UDT (user defined type) implementing any number of COM interfaces (including the mandatory IUnknown). The coclasses are then packaged into a binary home, which is physically represented as a DLL or EXE file. Once all the (known) bugs have been squashed out of the code, the COM binary eventually ends up on some user's computer, ready to be accessed by other programs.

The versioning problem in COM revolves around the fact that the COM runtime offers no intrinsic support to enforce that the correct version of a binary server is loaded for the calling client. It is true that a COM programmer can modify the version of the type library, update the registry to reflect these changes, and even reengineer the client's code base to reference a particular library. But, the fact remains that these are tasks delegated to the programmer and typically require rebuilding the code base. As many of you have learned the hard way, this is far from ideal.

Assume that you have jumped through the necessary hoops to try to ensure the COM client activates the correct version of a COM binary. Your worries are far from over given that some other application may be installed on the target machine that overrides your carefully configured registry entries (and maybe even replaces a COM server or two with an earlier version during the process). Mysteriously, your client application may now fail to operate.

For example, if you have 10 applications that all require the use of MyCOMServer.dll version 1.4, and another application installs MyCOMServer.dll version 2.0, all 10 applications are at risk of breaking. This is because we cannot be assured of complete backward compatibility. In a perfect world, all versions of a given COM binary are fully compatible with previous versions. In practice however, keeping COM servers (and software in general) completely backward compatible is extremely difficult.

The lump sum of each of these versioning issues is lovingly referred to as 'DLL Hell' (which, by the way, is not limited to COM DLLs; traditional C DLLs suffer the same hellish existence). As you'll see during the course of this chapter, the .NET framework solves this nightmare by using a number of techniques including side-by-side execution and a very robust (yet very simple) versioning scheme.

In a nutshell, .NET allows multiple versions of the same binary to be installed on the same target machine. Therefore, under .NET, if client A requires MyDotNETServer.dll version 1.4 and client B demands MyDotNETServer.dll version 2.0, the correct version is loaded for the respective client automatically. You are also able to bind to a specific version using an application configuration file.

Problem: COM Deployment

The COM runtime is a rather temperamental service. When a COM client wishes to make use of a coclass, the first step is to load the COM libraries for use by a given thread by calling CoInitialize(). At this point, the client makes additional calls to the COM runtime (e.g., CoCreateInstance(), CoGetClassObject() and so forth) to load a given binary into memory. The end result is that the COM client receives an interface reference that is then used to manipulate the contained coclass.

In order for the COM runtime to locate and load a binary, the COM server must be configured correctly on the target machine. From a high level, registering a COM server sounds so simple: Build an installation program (or make use of a system supplied registration tool) to trigger the correct logic in the COM binary (DllRegisterServer() for DLLs or WinMain() for EXEs) and call it a day. However, as you may know, a COM server requires a vast number of registration entries to be made. Typically, every COM class (CLSID), interface (IID), library (LIBID), and application (AppID) must be inserted into the system registry.

The key point to keep in mind is that the relationship between the binary image and the correct registry entries is extremely loose, and therefore extremely fragile. In COM, the location of the binary image (e.g., MyServer.dll) is entirely separate from the massive number of registry entries that completely describe the component. Therefore, if the end user were to relocate (or rename) a COM server, the entire system breaks, as the registration entries are now out of sync.

The fact that classic COM servers require a number of external registration details also introduces another deployment difficulty: The same entries must be made on every machine referencing the server. Thus, if you have installed your COM binary on a remote machine, and if you have 100 client machines accessing this COM server, this means 101 machines must be configured correctly. To say the least, this is a massive pain in the neck.

The .NET platform makes the process of deploying an application extremely simple given the fact that .NET binaries (i.e., assemblies) are not registered in the system registry at all. Plain and simple. Instead, assemblies are completely self-describing entities. Deploying a .NET application can be (and most often is) as simple as copying the files that compose the application to some location on the machine, and running your program.. In short, be prepared to bid a fond farewell to HKEY_CLASSES_ROOT.

An Overview of .NET Assemblies

Now that you understand the problem, let's check out the solution. .NET applications are constructed by piecing together any number of assemblies. Simply put, an assembly is nothing more than a versioned, self-describing binary (DLL or EXE) containing some collection of types (classes, interfaces, structures, etc.) and optional recourses (images, string tables and whatnot). One thing to be painfully aware of right now, is that the internal organization of a .NET assembly is nothing like the internal organization of a classic COM server (regardless of the shared file extensions).

For example, an in-process COM server exports four functions (DllCanUnloadNow(), DllGetClassObject(), DllRegisterServer() and DllUnregisterServer()), in order to allow the COM runtime to access its contents. .NET DLLs on the other hand require only one function export, DllMain().

Local COM servers define WinMain() as the sole entry point into the EXE, which is implemented to test for various command line parameters to perform the same duties as a COM DLL. Not so under the .NET protocol. Although .NET EXE binaries do provide a WinMain() entry point (or main() for console applications), the behind-the-scenes logic is entirely different.

The physical format of a .NET binary is actually more similar to a traditional portable executable (PE) and COFF (Common Object File Format) file formats. The true difference is that a traditional PE / COFF files contains instructions that target a specific platform and specific CPU. In contrast, .NET binaries contain code constructed using Microsoft Intermediate Language (MSIL or simply IL), which is platform- and CPU-agnostic. At runtime, the internal IL is compiled on the fly (using a just-in-time compiler) to platform and CPU specific instructions. This is a powerful extension of classic COM in that .NET assemblies are poised to be platform neutral entities that are not necessarily tied to the Windows operating system.

In addition to raw IL, recall that an assembly also contains metadata that completely describes each type living in the assembly, as well as the full set of members supported by each type. For example, if you created a class named JoyStick using some .NET aware language, the corresponding compiler emits metadata describing all the fields, methods, properties, and events defined by this custom type. The .NET runtime uses this metadata to resolve the location of types (and their members) within the binary, create object instances, as well as to facilitate remote method invocations.

Unlike traditional file formats or classic COM server, an assembly must contain an associated manifest (also referred to as 'assembly metadata'). The manifest documents each module within the assembly, establishes the version of the assembly, and also documents any external assemblies referenced by the current assembly (unlike a classic COM type library that does not document required external dependencies). Given this, a .NET assembly is completely self-describing.

Single File and Multifile Assemblies

Under the hood, a given assembly can be composed of multiple modules. A module is really nothing more than a generic name for a valid file. In this light, an assembly can be viewed as a unit of deployment (often termed a 'logical DLL'). In many situations, an assembly is in fact composed of a single module. In this case, there is a one-to-one correspondence between the (logical) assembly and the underlying (physical) binary, as shown in Figure 6-1.

When you create an assembly that is composed of multiple files, you gain efficient code download. For example, assume you have a remote client that is referencing a multifile assembly composed of three modules. If the remote application references only one of these modules, the .NET runtime only downloads the currently referenced file. If each module is 1 MB in size, I'm sure you can see the benefits.

Understand that multifile assemblies are not literally linked together into a new (larger) file. Rather, multifile assemblies are logically related by information contained in the corresponding manifest. On a related note, multifile assemblies contain a single manifest that may be placed in a standalone file, but is more typically bundled into one of the related modules. The big picture is seen in Figure 6-2.

This text is not concerned with the construction of multifile assemblies. However, be aware that online Help does document the process (which boils down to little more than passing the /addmodule flag to the C# compiler).

Two Views of an Assembly: Physical and Logical

As you begin to work with .NET binaries, it can be helpful to regard an assembly (both single file and multifile) as having two conceptual views. When you build an assembly, you are interested in the physical view. In this case, the assembly can be realized as some number of files that contain your custom types and resources (Figure 6-3).

As an assembly consumer, you are interested in a logical view of the assembly (Figure 6-4). In this case, you can understand an assembly as a versioned collection of public types that you can use in your current application (recall that 'internal' types can only be referenced by the assembly in which they are defined):

For example, the kind folks in Redmond who developed System.Drawing.dll created a physical assembly for you to consume in your applications. However, although System.Drawing.dll can be physically viewed as a binary DLL, you logically regard this assembly as a collection of related types. Of course, ILDasm.exe is the tool of choice when you are interested in discovering the logical layout of a given assembly (Figure 6-5).

The chances are good that you will play the role of both an assembly builder and assembly consumer, as is the case throughout this book. However, before digging into the code, let's briefly examine some of the core benefits of this new file format.

Assemblies Promote Code Reuse

Assemblies contain code that is executed by the .NET runtime. As you might imagine, the types and resources contained within an assembly can be shared and reused by multiple applications, much like a traditional COM binary. Unlike COM, it is possible to configure 'private' assemblies as well (in fact, this is the default behavior). Private assemblies are intended to be used only by a single application on a given machine. As you will see, private assemblies greatly simplify the deployment and versioning of your applications.

Like COM, binary reuse under the .NET platform honors the ideal of language independence. C# is one of numerous languages capable of building managed code, with even more languages to come. When a .NET-aware language adheres to the rules of the Common Language Specification (CLS), your choice of language becomes little more than a personal preference.

Therefore, it is not only possible to reuse types between languages, but to extend types across languages as well. In classic COM, developers were unable to derive COM object A from COM object B (even if both types were developed in the same language). In short, classic COM did not support classical inheritance (the 'is-a' relationship). Later in this chapter you see an example of cross-language inheritance.

Assemblies Establish a Type Boundary

Assemblies are used to define a boundary for the types (and resources) they contain. In .NET, the identity of a given type is defined (in part) by the assembly in which it resides. Therefore, if two assemblies each define an identically named type (class, structure, or whatnot) they are considered independent entities in the .NET universe.

Assemblies Are Versionable and Self-Describing Entities

As mentioned, in the world of COM, the developer is in charge of correctly versioning a binary. For example, to ensure binary compatibility between MyComServer.dll version 1.0 and MyComServer.dll version 2.4, the programmer must use basic common sense to ensure interface definitions remained unaltered or run the risk of breaking client code. While a healthy dose of versioning common sense also comes in handy under the .NET universe, the problem with the COM versioning scheme is that these programmer-defined techniques are not enforced by the runtime.

Another major headache with current versioning practices is that COM does not provide a way for a binary server to explicitly list the set of other binaries that must be present for it to function correctly. If an end user mistakenly moves, renames, or deletes a dependency, the solution fails. Under .NET, an assembly's manifest is the entity in charge of explicitly listing all internal and external contingencies.

Each assembly has a version identifier that applies to all types and all resources contained within each module of the assembly. Using a version identifier the runtime is able to ensure that the correct assembly is loaded on behalf of the calling client, using a well defined versioning policy (detailed later). An assembly's version identifier is composed of two basic pieces: A friendly text string (termed the informational version) and a numerical identifier (termed the compatibility version).

For example, assume you have created a new assembly with an informational string of 'MyInterestingTypes.' This same assembly would also define a compatibility number, such as 1.0.70.3. The compatibility version number always takes the same general format (four numbers separated by periods). The first and second numbers identify the major and minor version of the assembly (1.0 in this case). The third value (70) marks the build number, followed by the current revision number (3).

As you discover later in this chapter, the .NET runtime makes use of an assembly's version to ensure the correct binary is loaded on behalf of the client (provided that the assembly is shared). Because the manifest explicitly lists all external dependencies, the runtime is able to determine the 'last known good' configuration (i.e., the set of versioned assemblies that are known to function correctly).

Assemblies Define a Security Context

An assembly may also contain security information. Under the architecture of the .NET runtime, security measures are scoped at the assembly level. For example, if AssemblyA wishes to use a class contained within AssemblyB, AssemblyB is the entity that chooses to provide access (or not). The security constraints defined by an assembly are explicitly listed within its manifest. While a treatment of .NET security measures is outside the mission of this text, simply be aware that access to an assembly's contents is verified using assembly metadata.

Assemblies Enable Side-by-Side Execution

Perhaps the biggest advantage of the .NET assembly is the ability of multiple versions of the same assembly to be loaded (and understood) by the runtime. Thus, it is possible to install and load multiple versions of the same assembly on a single machine. In this way, clients are isolated from other incompatible versions of the same assembly.

Furthermore, it is possible to control which version of a (shared) assembly should be loaded using application configuration files. These files are little more than a simple text file describing (via XML syntax) the version, and specific location, of the assembly to be loaded on behalf of the calling application. You learn how to author application configuration files later in this chapter.

Building a Single File Test Assembly

Now that you have a better understanding of .NET assemblies, let's build a minimal and complete code library using C#. Physically, this will be a single file assembly named CarLibrary. To build a code library using the Visual Studio.NET IDE, you would select a new Class Library project workspace (Figure 6-6).

The design of our automobile library begins with an abstract base class named Car that defines a number of protected data members exposed through custom properties. This class has a single abstract method named TurboBoost() and makes use of a single enumeration (EngineState). Here is the initial definition of the CarLibrary namespace:

// Our first code library (CarLibrary.dll)
namespace CarLibrary
{
using System;

public enum EngineState      // Holds the state of the engine.
{
     engineAlive,
     engineDead
}

public abstract class Car    // The abstract base class in the hierarchy.
{
     // Protected state data.
     protected string petName;
     protected short currSpeed;
     protected short maxSpeed;
     protected EngineState egnState;

     public Car(){egnState = EngineState.engineAlive;}
     public Car(string name, short max, short curr)
     {
          egnState = EngineState.engineAlive;
          petName = name; maxSpeed = max; currSpeed = curr;
     }

     public string PetName
     {
          get { return petName; }
          set { petName = value; }
     }
     public short CurrSpeed
     {
          get { return currSpeed; }
          set { currSpeed = value; }
     }

     public short MaxSpeed
     { get { return maxSpeed; } }

     public EngineState EngineState
     { get { return egnState; } }

     public abstract void TurboBoost();
}
}

Now assume that you have two direct descendents of the Car type named MiniVan and SportsCar. Each implements the abstract TurboBoost() method in an appropriate manner:

namespace CarLibrary
{
using System;
using System.Windows.Forms;     // Needed for MessageBox definition.

// The SportsCar
public class SportsCar : Car
{
     // Ctors.
     public SportsCar(){}
     public SportsCar(string name, short max, short curr)
          : base (name, max, curr){}

     // TurboBoost impl.
     public override void TurboBoost()
     {
          MessageBox.Show("Ramming speed!", "Faster is better. . .");
     }
}

// The MiniVan
public class MiniVan : Car
{
     // Ctors.
     public MiniVan(){}
     public MiniVan(string name, short max, short curr)
          : base (name, max, curr){}

     // TurboBoost impl.
     public override void TurboBoost()
     {
          // Minivans have poor turbo capabilities!
          egnState = EngineState.engineDead;
          MessageBox.Show("Time to call AAA", "Your car is dead");
     }
}
}

Notice how each subclass implements TurboBoost() using the MessageBox class, which is defined in the System.Windows.Forms.dll assembly. In order for your assembly to make use of the types defined within this assembly, the CarLibrary project must include a reference to this binary using the 'Project | Add Reference' menu selection (Figure 6-7). In Chapter 8, the System.Windows.Forms namespace is described in detail. As you can tell by the name of the namespace, this assembly contains numerous types to help you build GUI applications. For now, the MessageBox class is all you need to concern yourself with. If you are following along, go ahead and compile your new code library.

A C# Client Application

Because each of our automobiles has been declared 'public,' other binaries are able to use our custom classes. In a moment, you learn how to make use of these types from other .NET aware languages such as Visual Basic. Until then, let's create a C# client. Begin by creating a new C# Console Application project (CSharpCarClient). Next, set a reference to your CarLibrary.dll, using the Browse button to navigate to the location of your custom assembly (again using the Add Reference dialog). Once you add a reference to your CarLibrary assembly, the Visual Studio.NET IDE responds by making a full copy of the referenced assembly and placing it into your Debug folder (assuming, of course, you have configured a debug build) (Figure 6-8).

Obviously this is a huge change from classic COM, where the resolution of the binary is achieved using the system registry.

Now that our client application has been configured to reference the CarLibrary assembly, you are free to create a class that makes use of these types. Here is a test drive (pun intended):

// Our first taste of binary reuse.
namespace CSharpCarClient
{
using System;

// Make use of the CarLib types!
using CarLibrary;

public class CarClient
{
     public static int Main(string[] args)
     {
          // Make a sports car.
          SportsCar viper = new SportsCar("Viper", 240, 40);
          viper.TurboBoost();

          // Make a minivan.
          MiniVan mv = new MiniVan();
          mv.TurboBoost();

          return 0;
     }
}
}

This code looks just like the other applications developed thus far. The only point of interest is that the C# client application is now making use of types defined within a unique assembly. Go ahead and run your program. As you would expect, the execution of this program results in the display of two message boxes.

A Visual Basic.NET Client Application

When you install Visual Studio.NET, you receive four languages that are capable of building managed code: JScript.NET, C++ with managed extensions (MC++), C# and Visual Basic.NET. A nice feature of Visual Studio.NET is that all languages share the same IDE. Therefore, Visual Basic.NET, ATL, C#, and MFC programmers all make use of a common development environment. Given this fact, the process of building a Visual Basic.NET application making use of the CarLibrary is simple. Assume a new VB.NET Windows Application project workspace named VBCarClient (Figure 6-9) has been created. Similar to Visual Basic 6.0, this project workspace provides a design time template used to build the GUI of the main window. However, VB.NET is a completely different animal. The template you are looking at is actually a subclass of the Form type, which is quite different from a VB 6.0 Form object (more details in Chaper 8).

Now, set a reference to the C# CarLibrary, again using the Add Reference dialog. Like C#, VB.NET requires you to list each namespace used within your project. However, VB.NET makes use of the 'imports' keyword rather than the C# 'using' directive. Thus, open the code window for your Form and add the following:

VB
' Like C#, VB.NET needs to 'see' the namespaces used by a given class.
Imports System
Imports System.Collections
. . .
Imports CarLibrary

Using the design time template, construct a minimal and complete user interface to exercise your automobile types (Figure 6-10). Two buttons should fit the bill (simply select the Button widget from the Toolbox and draw it on the Form object).

The next step is to add event handlers to capture the Click event of each Button object. To do so, simply double-click each button on the Form. The IDE responds by writing stub code that will be called when a button is clicked. Here is some sample code:

VB
' A little bit of VB.NET!
Protected Sub btnMiniVan_Click(ByVal sender As Object, 
                       ByVal e As System.EventArgs) Handles btnMiniVan.Click

        Dim sc As New MiniVan()
        sc.TurboBoost() 
End Sub

Protected Sub btnCar_Click(ByVal sender As Object, 
                     ByVal e As System.EventArgs) Handles btnCar.Click

        Dim sc As New SportsCar()
        sc.TurboBoost()      
End Sub

Although the goal of this book is not to turn you into a powerhouse VB.NET developer, here is one point of interest. Notice how each Car subclass is created using the New keyword. Unlike VB 6.0 however, classes now have true constructors! Therefore, the empty parentheses suffixed on the class name do indeed invoke a given constructor on the class. As you would expect, when you run the program, each automobile responds appropriately.

Cross-Language Inheritance

A very sexy aspect of .NET development is the notion of cross-language inheritance. To illustrate, let's create a new VB.NET class that derives from CarLibrary.SportsCar. Impossible you say? Well, if you were using Visual Basic 6.0 this would be the case. However with the advent of VB.NET, programmers are able to use the same object-oriented features found in C#, Java and C++, including classical inheritance (i.e., the 'is-a' relationship).

To illustrate, add a new class named PerformanceCar to your current VB.NET client application (using the 'Project | Add Class' menu selection). In the code that follows, notice you are deriving from the C# Car type using the VB.NET 'Inherits' keyword. As you recall, the Car class defined an abstract TurboBoost() method, which we implement using the VB.NET 'Overrides' keyword:

VB
' Yes, VB.NET supports each pillar of OOP!
Imports CarLibrary
Imports System.Windows.Forms

' This VB type is deriving from the C# SportsCar!
Public Class PerformanceCar
       Inherits CarLibrary.SportsCar
    
           ' Implementation of abstract Car method.
           Overrides Sub TurboBoost()
                MessageBox.Show("Blistering speed", "VB PerformanceCar says")
           End Sub
End Class

If we update our existing Form to include an additional Button to exercise the performance car, we could write the following test code:

VB
Protected Sub btnPreCar_Click(ByVal sender As Object, 
                     ByVal e As System.EventArgs) Handles btnPerfCar.Click
            
            Dim pc As New PerformanceCar()
            pc.PetName = "Hank"     ' Inherited property.

            ' Display base class.
            MessageBox.Show(pc.GetType().BaseType.ToString(), "Base class of Perf car")

            ' Custom Implementation of Car.TurboBoost()
            pc.TurboBoost()
End Sub

Notice that we are able to identify our base class programmatically (Figure 6-11).

Excellent! At this point you have begun the process of breaking your applications into discrete binary building blocks. Given the language-independent nature of .NET, any language targeting the runtime is able to create (and extend) the types described within a given assembly.

Exploring the CarLibrary's Manifest

At this point, you have successfully created a single file assembly and two client applications. Your next order of business is to gain a deeper understanding of how .NET assemblies are constructed under the hood. To begin, recall that every assembly contains an associated manifest, which can be regarded as the Rosetta stone of .NET. A manifest contains metadata that specifies the name and version of the assembly, as well as a listing of all internal and external modules that compose the assembly as a whole. Additionally, a manifest may contain culture information (used for internalization), a corresponding 'strong name' (required by shared assemblies) and optional security and resource information (we will examine the .NET resource format in Chapter 10).

.NET aware compilers (such as csc.exe) automatically create a manifest at compile time. As you see in Chapter 7, it is possible to augment the compiler-generated manifest using attribute-based programming techniques. For now, go ahead and load the CarLibrary assembly into ILDasm.exe. As you can see, this tool has read the metadata to display relevant information for each type (Figure 6-12).

Now, open the manifest by double clicking on the MANIFEST icon (Figure 6-13).

The first code block contained in a manifest is used to specify all external assemblies that are required by the current assembly to function correctly. As you recall, CarLibrary.dll made use of mscorlib.dll and System.Windows.Forms.dll, each of which are marked in the manifest using the [.assembly extern] tag:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4
  .ver 1:0:2411:0
}

.assembly extern System.Windows.Forms
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                        // .z\V.4..
  .ver 1:0:2411:0
}

Here, each [.assembly extern] block is colored by the [.publickeytoken] and [.ver] directives. The [.publickeytoken] instruction is only present if the assembly has been configured as a shared assembly and is used to reference the 'strong name' of the shared assembly (more details later). [.ver] is (of course) the numerical version identifier.

After enumerating each of the external references, the manifest then enumerates each module contained in the assembly. Given that the CarLibrary is a single file assembly, you will find exactly one [.module] tag. This manifest also lists a number of attributes (marked with the [.custom] tag) such as company name, trademark and so forth, all of which are currently empty (more information on these attributes in Chapter 7):

.assembly CarLibrary
{
  .custom instance void [mscorlib]
System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyConfigurationAttribute::.ctor(string)=( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 )
  .custom instance void [mscorlib]
System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 )
 
  .hash algorithm 0x00008004
  .ver 1:0:454:30104
}
.module CarLibrary.dll

Here, you can see that the [.assembly] tag is used to mark the friendly name of your custom assembly (CarLibrary). Like external declarations, the [.ver] tag defines the compatibility version number for this assembly, where [.hash] marks the file's generated hash code. Do note that the CarLibrary assembly does not define an [.publickeytoken] tag, given that CarLibrary has not been configured as a shared assembly.

To summarize the tags that dwell in the assembly manifest, ponder Table 6-1.

Table 6-1. Manifest IL Tags

Manifest Declaration Tag 

Meaning in Life

.assembly

Marks the assembly declaration, indicating that the file is an assembly.

.file

Marks extra files in the same assembly.

.class extern 

Classes exported by the assembly but declared in another module.

.exeloc 

Indicates the location of the executable for the assembly.

.manifestres

Indicates the manifest resources (if any). You see this tag in action in Chapter 9 (GDI+).

.module

Module declaration, indicating that the file is a module (i.e., a .NET binary with no manifest) and not an assembly.

.module extern

Modules of this assembly contain items referenced in this module.

Assembly extern

The assembly reference indicates another assembly containing items referenced by this module.

.publickey

Contains the actual bytes of the public key.

.publickeytoken

Contains a token of the actual public key.

Exploring the CarLibrary's Types

Recall that an assembly does not contain platform specific instructions, but rather platform agnostic intermediate language (IL). When the .NET runtime loads an assembly into memory, the underlying IL is compiled (using the JIT compiler) into instructions that can be understood by the target platform. Also recall that in addition to raw IL and the assembly manifest, an assembly contains metadata that describes and members of each type contained within a given module.

For example, if you were to double click the TurboBoost() method of the SportsCar class, ILDasm.exe would open a new window showing the raw IL instructions. Notice in the following screen shot, that the [.method] tag is used to identify (of course) a method defined by the SportsCar type (Figure 6-14). As you might expect, public data defined by a type is marked with the [.field] tag (Figure 6-15). Recall that the Car class defined a set of protected data, such as currSpeed (note that the 'family' tag signifies protected data).

Properties are also marked with the [.property] tag (Figure 6-16). The figure shows the IL describing the public property that provides access to the underlying currSpeed data point (note the read/write nature of the CurrSpeed property is marked by .get and .set tags):

If you now select the 'Ctrl + M' keystroke, ILDasm.exe would display the metadata for each type (Figure 6-17).

Using this metadata, the .NET runtime is able to locate and construct object instances, and invoke methods. Various tools (such as Visual Studio.NET) make use of metadata at design time in order to validate the number of (and type of) parameters during compilation. To summarize the story so far, make sure the following points are clear in your mind:

  • An assembly is a versioned, self-describing set of modules. Each module contains some number of types and optional resources.
  • Every assembly contains metadata that describes all types within a given module. The .NET runtime (as well as numerous design time tools) read the metadata to locate and create objects, validate method calls, activate IntelliSense, and so on.
  • Every assembly contains a manifest that enumerates the set of all internal and external files required by the binary, version information as well as other assembly-centric details.

Next you need to distinguish between private and shared assemblies. If you are coming into the .NET paradigm from a classic COM perspective, be prepared for some significant changes.

Understanding Private Assemblies

Formally speaking, an assembly is either 'private' or 'shared.' The good news is each variation has the same underlying structure (i.e., some number of modules and an associated manifest). Furthermore, each flavor of assembly provides the same kind of services (for example, access to some number of public types). The real differences between a private and shared assembly boils down to naming conventions, versioning policies, and deployment issues. Let's begin by examining the traits of a private assembly, which is far and away the most common of the two options.

Private assemblies are a collection of types that are only used by the application with which it has been deployed. For example, CarLibrary.dll is a private assembly used by the CSharpCarClient and VBCarClient applications. When you create a private assembly, the assumption is that the collection of types are only used by the 'owning' application, and not shared with other applications on the system.

Private assemblies are required to be located within the main directory of the owning application (termed the application directory) or a subdirectory thereof. For example, recall that when you set a reference to the CarLibrary assembly (as we did in the CSharpCarClient and VBCarClient applications), the Visual Studio.NET IDE responded by making a full copy of the assembly that was placed it in your project's application directory. This is the default behavior, as private assemblies are assumed to be deployment option of choice.

Note the painfully stark contrast to classic COM. There is no need to register any items under HKEY_CLASSES_ROOT and no need to enter a hard-coded path to the binary using an InprocServer32 or LocalServer32 listing. The resolution and loading of the private CarLibrary happens by virtue of the fact that the assembly is placed in the application directory. In fact, if you moved CSharpCarClient.exe and CarLibrary.dll to a new directory, the application would still run. To illustrate this point, copy these two files to your desktop and run the client (Figure 6-18).

Uninstalling (or replicating) an application that makes exclusive use of private assemblies is a no-brainer. Delete (or copy) the application folder. Unlike classic COM, you do not need to worry about dozens of orphaned registry settings. More important, you do not need to worry that the removal of private assemblies will break any other applications on the machine!

Probing Basics

Later in this chapter, you are exposed to a number of gory details regarding location resolution of an assembly. Until then, the following overview should help prime the pump. Formally speaking, the .NET runtime resolves the location of a private assembly using a technique termed probing, which is much less invasive than it sounds. Probing is the process of mapping an external assembly reference (i.e., [.assembly extern]) to the correct corresponding binary file. For example, when the runtime reads the following line from the VBCarClient's manifest:

.assembly extern CarLibrary
{
. . .
}

a search is made in the application directory for a file named CarLibrary.DLL. If a DLL binary cannot be located, an attempt is made to locate an EXE version (CarLibrary.EXE). If neither of these files can be found, a further examination ensues for a shared assembly (examined in just a bit).

The Identity of a Private Assembly

The identity of a private assembly consists of a friendly string name and numerical version, both of which are recorded in the assembly manifest. The friendly name is created based on the name of the binary module that contains the assembly's manifest. For example, if you examine the manifest of the CarLibrary.dll assembly, you find the following (the exact version may vary):

.assembly CarLibrary as "CarLibrary"
{
. . .
.ver 1:0:454:30104
}

However, given the nature of a private assembly, it should make sense that the .NET runtime does not bother to apply any version policies when loading the assembly. The assumption is that private assemblies do not need to have any elaborate version checking, given that the client application is the only entity that "knows" of its existence. As an interesting corollary you should understand that it is (very) possible for a single machine to have multiple copies of the same private assembly in various application directories.

Private Assemblies and XML Configuration Files

When the .NET runtime is instructed to bind to an assembly, the first step is to determine the presence of an application configuration file. These optional files contain XML tags that control the binding behavior of the launching application. By law, configuration files must have the same name as the launching application and take a *.config file extension.

As mentioned, configuration files can be used to specify any optional subdirectories to be searched during the process of binding to private assemblies. As you have seen earlier in this chapter, a componentized .NET application can be deployed simply by placing all assemblies into the same directory as the launching application. Often, however, you may wish to deploy an application such that the application directory contains a number of related subdirectories, in order to give some meaningful structure to the application as a whole.

You see this all the time in commercial software. For example, assume our main directory is called MyRadApplication, which contains a number of subdirectories (\Images, \Bin, \SavedGames, \OtherCoolStuff). Using application configuration files, you can instruct the runtime where it should probe while attempting to locate the set of private assemblies used by the launching application.

To illustrate, let's create a simple configuration file for the previous CSharpCarClient application. Our goal is to move the referenced assembly (CarLibrary) from the Debug folder into a new subdirectory named Foo \ Bar. Go ahead and move this file now (Figure 6-19).

Now, create a new configuration file named CSharpCarClient.exe.config (notepad will do just fine) and save it into the same folder containing the CSharpCarClient.exe application. The beginning of an application configuration file is marked with the <Configuration. tag. Before the closing </Configuration. tag, specify an assemblyBinding row, which is used to specify alternative locations to search for a given assembly, using the privatePath attribute (FYI, multiple subdirectories can be specified using a semicolon delimited list):

XML
<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <probing privatePath="foo\bar"/>
        </assemblyBinding>
    </runtime>
</configuration>

Once you are done, save the file and launch the client. You will find that the CSharpCarClient application runs without a hitch. As a final test, change the name of your configuration file and attempt to run the program once again (Figure 6-20).

The client application silently fails. Recall that configuration files must have the same name as the launching application. Because you have renamed this file, the .NET runtime assumes you do not have a configuration file, and thus attempts to probe for the referenced assembly in the application directory (which it cannot locate).

Specifics of Binding to a Private Assembly

To wrap up the current discussion, let's formalize the specific steps involved in binding to a private assembly at runtime. First, a request to load an assembly may be either explicit or implicit. An implicit load request occurs whenever the manifest makes a direct reference to some external assembly. As you recall, external references are marked with the [.assembly extern] instruction:

// An implicit load request. . .
.assembly extern CarLibrary
{
. . .
}

An explicit load request occurs programmatically using System.Reflection.Assembly.Load().The Assembly class is examined in Chapter 7, but be aware that the Load() method allows you to specify the name, version, strong name, and culture information syntactically (note you are not required to specify each characteristic):

// An explicit load request. . .
Assembly asm = Assembly.Load("CarLibrary");

Collectively, the name, version, strong name, and culture information is termed an assembly reference (or simply AsmRef). The entity in charge of locating the correct assembly based on an AsmRef is termed the assembly resolver, which is a facility of the CLR.

As mentioned earlier, an application directory is nothing more than a folder on your hard drive (for example, C:\MyApp) that contains all the files for a given application. If necessary, an application directory may specify additional subdirectories (e.g., C:\MyApp\Bin, C:\MyApp\Tools, and so on) to establish a more stringent file hierarchy.

When a binding request is made, the runtime passes an AsmRef to the assembly resolver. If the resolver determines the AsmRef refers to a private assembly (meaning there is no strong name recorded in the manifest), the following steps are followed:

  1. First, the assembly resolver attempts to locate a configuration file in the application directory. As you will see, this file can specify additional subdirectories to include in the search, as well as establish a version policy to use for the current bind.
  2. If there is no configuration file, the runtime attempts to discover the correct assembly by examining the current application directory. If a configuration file does exist, any specified subdirectories are searched.
  3. If the assembly cannot be found within the application directory (or a specified subdirectory) the search stops here and a TypeLoadException exception is raised, as private assemblies are always located within the application directory (or a specified subdirectory).

To solidify this sequence of events, Figure 6-21 illustrates the process outlined above.

Again, as you can see, the location of a private assembly is fairly simply to resolve. If the application directory does not contain a configuration file, the assembly resolver simply looks for a binary that matches the correct string name. If the application directory does contain a configuration file, any specified subdirectories are also searched.

Understanding Shared Assemblies

Like a private assembly, a "shared" assembly is a collection of types and (optional) resources contained within some number of modules. The most obvious difference between shared and private assemblies is the fact that shared assemblies can be used by several clients on a single machine. Clearly, if you wish to create a machine-wide class library, a shared assembly is the way to go.

A shared assembly is typically not deployed within the same directory as the application making use of it. Rather, shared assemblies are installed into a machine-wide Global Assembly Cache, which lends itself to yet another colorful acronym in the programming universe: the GAC. The GAC itself is located under the <drive.: \ WinNT \ Assembly subdirectory (Figure 6-22).

This is yet another major difference between the COM and .NET architectures. In COM, shared applications can reside anywhere on a given machine, provided they are properly registered. Under .NET, shared assemblies are typically placed into a centralized well-known location (the GAC).

Unlike private assemblies, a shared assembly requires additional version information beyond the friendly text string. As you may have guessed, the .NET runtime does enforce version checking for a shared assembly before it is loaded on behalf of the calling application. In addition, a shared assembly must be assigned a "shared name" (also known as a "strong name").

Problems with Your GAC?

By way of a quick side note, as of Beta2 (on which this text is based) I have noticed that some of my development machines are unable to display the GAC correctly. The problem is that the GAC is a shell extension that requires the registration of a COM server named shfusion.dll. During installation, this server may fail to register correctly. If you are having problems opening the GAC on your machine, simply register this COM server using regsvr32.exe and you should be just fine.

Understanding Shared (Strong) Names

When you wish to create an assembly that can be used by numerous applications on a given machine, your first step is to create a unique shared name for the assembly. A shared name contains the following information:

  • A friendly string name and optional culture information (just like a private assembly).
  • A version identifier.
  • A public/private key pair.
  • A digital signature.

The composition of a shared name is based on standard public key cryptography. When you create a shared assembly, you must generate a public/private key pair (that you do momentarily). The key pair is included in the build cycle using a .NET aware compiler, which in turn lists a token of the public key in the assembly's manifest (via the [.publickeytoken] tag). The private key is not listed in the manifest, but rather, is signed with the public key. The resulting signature is stored in the assembly itself (in the case of a multifile assembly, the private key is stored with the file defining the manifest).

Now, assume some client has referenced this shared assembly (which is no different than referencing a private assembly). When the compiler generates the client binary, the public key is recorded in its manifest. At runtime, the .NET runtime ensures that both the client and the shared assembly are making using of the same key pair. If these keys are identical, the client application can rest assured that the correct assembly as been loaded. Figure 6-23 presents the basic picture.

As you might guess, there are additional details regarding key pairs. We really don't need more details for now, so check out online Help if you so choose.

Building a Shared Assembly

To generate a strong name for your assembly, you need to make use of the sn.exe (strong name) utility. Although this tool has numerous command line options, all we need to concern ourselves with is the "-k" argument, which instructs the tool to generate a new strong name key that will be saved to a specified file (Figure 6-24).

If you examine the contents of this new file (theKey.snk) you see the binary markings of the key pair (Figure 6-25).

To continue with the example, assume you have created a new C# Class Library called (of course) SharedAssembly, which contains the following class definition:

using System;
using System.Windows.Forms;

namespace SharedAssembly
{
public class VWMiniVan
{
     public VWMiniVan(){}

     public void Play60sTunes()
     {
          MessageBox.Show("What a loooong, strange trip it's been. . .");
     }

     private bool isBustedByTheFuzz = false;
     public bool Busted
     {
          get { return isBustedByTheFuzz; }
          set { isBustedByTheFuzz = value; }
     }
}
}

The next step is to record the public key in the assembly manifest. The easiest way to do so is to leverage the use of an attribute named AssemblyKeyFile. When you create a new C# project workspace, you will notice that one of your initial project files is named "AssemblyInfo.cs" (Figure 6-26).

This file contains a number of (initially empty) attributes that are consumed by a .NET aware compiler. If you examine this file, you find one such attribute named AssemblyKeyFile. To specify the strong name for a shared assembly, simply update the initial empty value with a string specifying the location of your *.snk file:

[assembly: AssemblyKeyFile(@"D:\SharedAssembly\theKey.snk")]

Using this assembly level attribute, the C# compiler now merges the necessary information into the corresponding manifest, as can be seen using ILDasm.exe (note the [.publickey] tag in Figure 6-27).

Installing Assemblies into the GAC

Once you have established a strong name for your shared assembly, the final step is to install it into the GAC. The simplest approach to install a private assembly into the GAC is to drag and drop the file(s) onto the active window (you are also free to make use of the gacutil.exe command line utility). SeeFigure 6-28.

Do be aware that you must have Administrative rights on the computer to install assemblies into the GAC. This is a good thing, in that it prevents the casual user from accidentally breaking existing applications.

The end result is that your assembly has now been placed into the GAC, and may be shared by multiple applications on the target machine. On a related note, when you wish to remove an assembly from the GAC, you may do so with a simple right-click (just select Delete from the Context menu).

Using a Shared Assembly

Now to prove the point, assume you have created a new C# Console application (called SharedAssemblyUser), set a reference to the SharedAssembly binary, and created the following class definition:

namespace SharedLibUser
{
using System;
using SharedAssembly;

public class SharedAsmUser
{
     public static int Main(string[] args)
     {
          try
          {
               VWMiniVan v = new VWMiniVan();
               v.Play60sTunes();
          } 
          catch(TypeLoadException e)
          {
               // Can't find assembly!
               Console.WriteLine(e.Message);
          }
          return 0;
     }
}
}

Recall, that when you reference a shared assembly, IDE automatically creates a local copy of the assembly for use by the client application. However, when you reference an assembly that contains a public key (as is the case with the SharedAssembly.dll), you do not receive a local copy. The assumption is that assemblies containing a public key are designed to be shared (and are therefore placed in the GAC).

Do be aware that the VS.NET IDE allows you to explicitly control the copying of a given assembly using the Properties window. For example, if you have set a reference to the external binary, select this assembly using the Solution Explorer and set Copy Local to false. This will delete the local copy (Figure 6-29).

Now run the client application once again. If all is well, everything should still function correctly, as the .NET runtime consulted the GAC during its quest to resolve the location of the requested assembly (Figure 6-30).

Understanding .NET Version Policies

As you have already learned, the .NET runtime does not bother to perform version checks for private assemblies. The versioning story changes significantly when a request is made to load a shared assembly. Given that the version of a shared assembly is of prime importance, let's review the composition of version numbers. As you recall, a version number is marked by four discrete parts (for example 2.0.2.11). Logically however, the .NET runtime is able to extract three meaningful bits of information regarding version compatibility, as illustrated in Figure 6-31.

Whenever two assemblies differ by either the major or minor version number (e.g., 2.0 vs. 2.5) they are considered to be completely incompatible with each other as far as the .NET runtime is concerned. When assemblies differ by major or minor numerical markings, you can assume significant changes have occurred (e.g., method name changes, types have been added or removed, parameters have changed, and so forth). Therefore, if a client is requesting a bind to version 2.0 but the GAC only contains version 2.5, the bind request fails (unless overridden by an application configuration file).

If two assemblies have identical major and minor version numbers, but have different revision numbers (e.g., 2.5.0.0 vs. 2.5.1.0) the .NET runtime assumes they might be compatible with each other (in other words, backward compatibility is assumed, but not guaranteed). By way of a concrete example, a Service Pack release typically involves modifying the revision number of an assembly.

Finally, you have the Quick Fix Engineering (QFE) number. When two assemblies differ only by their QFE value, the .NET runtime assumes they are fully compatible. QFEs are typically modified with the release of a software patch. The idea here is that all calling conventions (e.g., method names, parameters, supported interfaces, and so forth) are identical to previous versions.

Recording Version Information

One question you might be asking yourself at this point is where was this version number specified? Recall that every C# project defines a file named AssemblyInfo.cs. If you examine this file, you will see an attribute named AssemblyVersion, which is initially set to a string reading "1.0.*":

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

Every new C# projects begins life versioned at 1.0. As you build new versions of a shared assembly, part of your task is to update the four-part version number for your shared assembly. Do be aware that the IDE automatically increments the build and revision numbers (as marked by the '*' tag). If you wish to enforce an application-specific value for the assembly's build and/or revision, simply update accordingly:

[assembly: AssemblyVersion("1.0.0.0")]

Freezing the Current SharedAssembly

To really understand .NET versioning policies, we need to have a concrete example. The current goal is to update your previous SharedAssembly.dll to support additional functionality, update the version number, and then place the new version into the GAC. At this point, you are able to experiment with the use of application configuration files to specify various version policies, as well as side-by-side execution.

To begin, let's update the constructor of the VWMiniVan class to display a message verifying the current version:

public VWMiniVan()
{
     MessageBox.Show("Using version 1.0.0.0!", "Shared Car");
}

Next, update the AssemblyVersion attribute to be fully qualified to version 1.0.0.0 (as seen in the previous section). Go ahead and recompile the project.

The next thing you need to do is ensure that our original SharedAssembly.dll is removed from the GAC (go ahead and delete this assembly now). Next, move your existing 1.0.0.0 assembly into a new folder (I called mine Version1) to ensure you freeze this version (Figure 6-32).

Now (once again!) place this assembly back into the GAC. Notice that the version of this assembly is <1.0.0.0. (Figure 6-33).

Once version 1.0.0.0 of the SharedAssembly has been inserted into the GAC, right-click this assembly and select Properties from the context-sensitive pop-up menu. Verify that the path to this binary maps to the Version1 subdirectory. Finally, rebuild and run the current SharedAssemblyUser application. Things should continue to work just fine.

Building SharedAssembly Version 2.0

To illustrate the .NET version policy, let's modify the current SharedAssembly project. Update your VWMiniVan class with a new member (which makes use of a custom enumeration) to allow the user to play some more modern musical selections. Also be sure to update the message displayed from within the constructor logic.

// Which band do you want?
public enum BandName
{
     TonesOnTail, SkinnyPuppy, deftones, PTP
}

public class VWMiniVan
{
     public VWMiniVan()
     { MessageBox.Show("Using version 2.0.0.0!", "Shared Car"); }
. . .
     public void CrankGoodTunes(BandName band)
     {
          switch(band)
          {
               case BandName.deftones:
                    MessageBox.Show("So forget about me. . .");
                    break;
               case BandName.PTP:
                    MessageBox.Show("Tick tick tock. . .");
                    break;
               case  BandName.SkinnyPuppy:
                    MessageBox.Show("Water vapor, to air. . .");
                    break;
               case  BandName.TonesOnTail:
                    MessageBox.Show("Oooooh the rain. Oh the rain.");
                    break;
               default:
                    break;
          }
     } 
}

Before you compile, let's upgrade this version of this assembly to 2.0.0.0:

// Update your assemblyinfo.cs file as so. . .
[assembly: AssemblyVersion("2.0.0.0")]

If you look in your project's debug folder, you see that you have a new version of this assembly (2.0) while the previous version is safe in storage under the Version1 directory. Finally, let's install this new assembly into the GAC. Notice that you now have two versions of the same assembly (Figure 6-34).

Now that you have a distinctly versioned assembly recorded in the GAC, you can begin to work with application configuration files to control how a client binds to a given version. But first, a few words about the default binding policy.

Understanding the Default Version Policy

As mentioned earlier in the chapter, if a client is referencing a shared assembly, the major and minor versions must be identical if the bind is to succeed. However, the .NET runtime binds to a given assembly if the assembly reference differs by the revision or build numbers. This behavior is termed the default version policy and is used to ensure that a client always gets the latest and greatest service release (i.e., bug fix) of a given assembly. Thus, if the client's manifest explicitly requests version 1.0.0.0, but the GAC has a newer version by specifying a QFE (such as 1.0.2.2), the client automatically receives the most recent fix. In this way, a client application is guaranteed that the assembly that it is referencing is backward compatible, in addition to being as bug-free as possible.

Specifying Custom Version Policies

When you wish to dynamically control how an application binds to an assembly (such as disabling QFEs), you need to author an application configuration file. As you have already seen during the discussion of private assemblies, configuration files are blocks of XML that are used to customize the binding process. Recall that these files must have the same name as the owning application (with a *.config extension) and be placed directly in the application directory. In addition to the privatePath tag (used to specify where to probe for private assemblies), a configuration file may specify information for shared assemblies.

The first point of interest is using an application configuration file to specify a specific assembly version that is to be loaded, regardless of what may be listed in the corresponding manifest. When you wish to redirect a client to bind to an alternate shared assembly, you make use of the <dependentAssembly. and <bindingRedirect. attributes. For example, the following configuration file forces version 2.0.0.0:

XML
<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="sharedassembly" 
                         publicKeyToken="6c0646f072c6fe39" 
                         culture=""/>

                <bindingRedirect oldVersion= "1.0.0.0"
                                 newVersion= "2.0.0.0"/>

            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

Here, the oldVersion tag is used to specify the version that you wish to override. The newVersion tag marks a specific version to load.

To test this out yourself, create the previous configuration file and save it into the directory of the SharedAssemblyUser application (be sure you name this configuration file correctly). Now, run the program. You should see the message that appears in Figure 6-35.

If you update the newVersion attribute to 1.0.0.0, you now see the message that appears in Figure 6-36.

Way cool. What you have just observed is the notion of side-by-side execution mentioned earlier in the chapter. Because the .NET framework allows you to place multiple versions of the same assembly into the GAC, you can easily configure custom version policies as you (or a system administrator) see fit.

As you have seen, the .NET framework does indeed take the version of a shared assembly seriously. Through the use of application configuration files, you are able to control a number of details regarding which version of a given assembly should be loaded by an owning application. As you may expect, there are additional attributes that may be listed in an application's configuration file. Investigate these details as you wish. However, there is one final aspect to consider. . .

The Administrator Configuration File

The configuration files you have been examining in this chapter each have a common theme. They only apply to a specific application (that is why they had the same name as the owning application). The .NET framework does allow an additional type of configuration file called the administrator configuration file. Each .NET-aware machine has a file named "machine.config" that contains listings used to override any application-specific configuration files. As you might guess, reading this file is a great way to learn more *.config centric tags.

Now that you have an intimate understanding of .NET assemblies, let's switch gears completely and examine the related topics of application domains and multithreaded assemblies. Although this may seem like a drastic change of content, you will see that assemblies, application domains, and threads are interrelated.

Review of Traditional Win32 Thread Programming

Depending on your programming background, you may be extremely interested in building multithreaded binaries, could care less about building multithreaded binaries, or are a little unsure what multithreading means in the first place. In order to level the playing field, let's take the time to quickly review the basics of multithreading. Once you have reviewed multithreading from a traditional Win32 perspective, you will then come to understand how things have changed under the .NET platform.

To begin, recall that under traditional Win32, each application is hosted by a process. Understand that process is a generic term used to describe the set of external resources (such as a COM server) as well as the necessary memory allocations used by a given application. For each EXE loaded into memory, the operating system creates a separate and isolated memory partition (i.e., process) for use during its lifetime.

Every running process has at least one main "thread" that serves as the entry point for the application. Formally speaking, the first thread created in a given process is termed the primary thread. Simply put, a thread is a specific path of execution within the Win32 process. A traditional Windows applications defines the WinMain() method to function as the application's entry point. On the other hand, Console application provides the main() method for the same purpose.

Applications that contain only a single thread of execution are automatically "thread-safe" given the fact that there is only one thread that can access the data in the application at a given time. On the downside, a single-threaded application can appear a bit unresponsive to the end user if this single thread is performing a complex operation (such as printing out a lengthy text file, performing an exotic calculation, or connecting to a remote server).

Under Win32, it is possible for the primary thread to spawn additional secondary threads in the background, using a handful of Win32 API functions such as CreateThread(). Each thread (primary or secondary) becomes a unique path of execution in the process and has concurrent access to all data in that process. As you may have guessed, developers typically create additional threads to help improve the program's overall responsiveness.

Multithreaded applications provide the illusion that numerous activities are happening at more or less the same time. For example, you could spawn a background worker thread to perform a labor-intensive unit of work (again, such as printing a large text file). As this secondary thread is churning away, the main thread is still responsive to user input, which gives the entire process the potential of delivering greater performance. However, this is only a possibility. Too many threads in a single process can actually degrade performance, as the CPU must switch between the active threads in the process (which takes time).

In reality, multithreading is often a simple illusion provided by the operating system. Machines that host a single CPU do not have the ability to literally handle multiple threads at the same exact time. Rather, a single CPU will execute one thread for a unit of time (called a time-slice) based on the thread's priority level. When a thread's time-slice is up, the existing thread is suspended to allow the other thread to perform its business. In order for a thread to remember what was happening before it was kicked out of the way, each thread is given the ability to write to Thread Local Storage (TLS) and is provided a separate call stack, as illustrated in Figure 6-37.

Problem of Concurrency and Thread Synchronization

Beyond taking time, the process of switching between threads can cause additional problems. For example, assume a given thread is accessing a shared point of data, and in the process begins to modify it. Now assume that the first thread is told to wait, to allow another thread to access the same point of data. If the first thread was not finished with its task, the second thread may be modifying data that is in an unstable state.

To protect the application's data from possible corruption, the Win32 developer must make use of any number of Win32 threading primitives such as critical sections, mutexes or semaphores to synchronize access to shared data. Given this, multithreaded applications are much more volatile, as numerous threads can operate on the application's data at the same time. Unless the developer has accounted for this possibility using threading primitives (such as a critical section) the program may end up with a good amount of data corruption.

Although the .NET platform cannot make the difficulties of building robust multithreaded applications completely disappear, the process has been simplified considerably. Using types defined within the System.Threading namespace, you are able to spawn additional threads with minimal fuss and bother. Likewise, when it comes time to lock down shared points of data, you will find additional types that provide the same functionality as the Win32 threading primitives.

Understanding System.AppDomain

Before we examine the full details of the System.Threading namespace, we need to examine the concept of application domains. As you know, .NET applications are created by piecing together any number of related assemblies. However, unlike a traditional (non-.NET) Win32 EXE application, .NET applications are hosted by an entity termed an "application domain" (aka AppDomain). Be very aware that the term AppDomain is not a synonym for a Win32 process.

In reality, a single process can host any number of AppDomains, each of which is fully and completely isolated from other AppDomains within this process (or any other process). Applications that run in different AppDomains are unable to share any information of any kind (global variables or static fields) unless they make use of the .NET remoting protocol. The big picture is shown in Figure 6-38.

Notice the stark difference from a traditional Win32 process. Under .NET, a single process may contain multiple AppDomains. Each AppDomain may contain multiple threads. In some respects, this layout is reminiscent of the "apartment" architecture of classic COM. Of course, .NET AppDomains are managed types whereas the COM apartment architecture is built on an unmanaged (and much more complex) architecture.

AppDomains are programmatically represented by the System.AppDomain type. Some core members to be aware of are shown in Table 6-2.

Fun with AppDomains

As you can see, the members of AppDomain provide numerous process-like behaviors, with a .NET flair. To illustrate some of this flair, consider the following namespace definition:

namespace MyAppDomain
{
     using System;
     using System.Windows.Forms;

     // Need this namespace to work with the Assembly type.
     using System.Reflection;

     public class MyAppDomain
     {
          public static void PrintAllAssemblies()
          {
               // Ask the current AppDomain for a list of all
               // loaded assemblies.
               AppDomain ad = AppDomain.CurrentDomain;
               Assembly[] loadedAssemblies = ad.GetAssemblies();

               Console.WriteLine("Here are the assemblies loaded in " + 
                                 "this appdomain\n");

               // Now print the fully qualified name of each one.
               foreach(Assembly a in loadedAssemblies)
               {
                    Console.WriteLine(a.FullName);
               }
          }
          public static int Main(string[] args)
          {
               // Force the loading of the Windows Forms assembly.
               MessageBox.Show("Loaded System.Windows.Forms.dll");
               PrintAllAssemblies();
               return 0;
          }
     }
}

First of all, notice that you are making use of a new namespace, System.Reflection. Full details of this namespace are seen in Chapter 7. For the time being, just understand that this namespace defines the Assembly type, which we need access to given the role of the PrintAllAssemblies() method.

This static member obtains a reference to the hosting AppDomain, and enumerates over the list of loaded assemblies. To make it more interesting, notice that the Main() method launches a message box in order to force the assembly resolver to load the System.Windows.Forms.dll assembly (which in turn loads other referenced assemblies). Figure 6-39 shows the output.

System.Threading Namespace

The System.Threading namespace provides a number of types that enable multithreaded programming. In addition to providing types that represent a specific thread, this namespace also defines types that can manage a collection of threads (ThreadPool), a simple (non-GUI based) Timer class and numerous types to provide synchronized access to shared data. Table 6-3 lists some (but not all) of the core items.

Table 6-3. Select Types of the System.Treading Namespace

System.Threading Type   

Meaning in Life

Interlocked   

The Interlocked class is used to provide synchronized access to shared data.

Monitor   

Provides the synchronization of threading objects using locks and wait/signals.

Mutex   

Synchronization primitive that can be used for inter process synchronization.

Thread   

Represents a thread that executes within the CLR. Using this type, you are able to spawn additional threads in the owning AppDomain.

ThreadPool   

This type manages related threads in a given process.

Timer   

Specifies a delegate to be called at a specified time. The wait operation is performed by a thread in the thread pool.

WaitHandle   

Represents all synchronization objects (that allow multiple wait) in the runtime.

ThreadStart   

The ThreadStart class is a delegate that points to the method that should be executed first when a thread is started.

TimerCallback   

Delegate for the Timers.

WaitCallback   

This class is a Delegate that defines the callback method for ThreadPool user work items.

Examining the Thread Class

The most primitive of all types in the System.Threading namespace is Thread. This class represents an object-oriented wrapper around a given path of execution within a particular AppDomain. This type defines a number of methods (both static and shared) that allow you to create new threads from a current thread, as well as suspend, stop, and destroy a given thread. First, consider the list of core static members given in Table 6-4.

Table 6-4. Static Members of the Thread Type

Thread Static Member   

Meaning in Life

CurrentThread   

This (read-only) property returns a reference to the currently running thread.

GetData()   

Retrieves the value from the specified slot on the current SetData() thread, for that thread's current domain.

GetDomain()   

Returns a reference to the current AppDomain (or the ID of GetDomainID() this domain) in which the current thread is running.

Sleep()   

Suspends the current thread for a specified time.

Thread also supports the object level members shown in Table 6-5.

Table 6-5. Object Methods of the Thread Type

 

Thread Instance Level Member   

Meaning in Life

IsAlive   

This property returns a boolean that indicates if this thread has been started.

IsBackground   

Gets or sets a value indicating whether or not this thread is a background thread.

Name   

This property allows you to establish a friendly textual name of the thread.

Priority   

Gets or Sets the priority of a thread, which may be assigned a value from the ThreadPriority enumeration.

ThreadState   

Gets the state of this thread, which may be assigned a value from the ThreadState enumeration.

Interrupt()   

Interrupts the current thread.

Join()   

Instructs the thread to wait for a given thread.

Resume()   

Resumes a thread that has been suspended.

Start()   

Begins execution of the thread that is specified by the ThreadStart delegate.

Suspend()   

Suspends the thread. If the thread is already suspended, a call to Suspend() has no effect.

Spawning Secondary Threads

When you wish to create additional threads to carry on some unit of work, you need to interact with the Thread class as well as a special threading-related delegate named ThreadStart. The general process is quite simple. To begin, you need to create a function to perform the background work. To keep things well focused, let's build a simple helper class that simply prints out a series of numbers by way of the DoSomeWork() member function:

internal class WorkerClass
{ 
     public void DoSomeWork()
     {
          // Get some information about this worker thread.
          Console.WriteLine("ID of worker thread is: {0}",
                            Thread.CurrentThread.GetHashCode()); 

          // Do the work.
          Console.Write("Worker says: ");
          for(int i = 0; i < 10; i++)
          {
               Console.Write(i + ", ");
          }
          Console.WriteLine();
     }
}

Now assume you have another class (MainClass) that creates a new instance of WorkerClass. In order for the MainClass to continue processing its workflow, it creates and starts a new Thread that is used by the worker. In the code below, notice the Thread type requests a new ThreadStart delegate type:

public class MainClass
{
     public static int Main(string[] args)
     {
          // Get some information about the current thread.
          Console.WriteLine("ID of primary thread is: {0}",
                            Thread.CurrentThread.GetHashCode());

          // Make worker class.
          WorkerClass w = new WorkerClass();

          // Now make (and start) the background thread.
          Thread backgroundThread = 
                 new Thread(new ThreadStart(w.DoSomeWork));

          backgroundThread.Start();
 
          return 0;
     }
}

If you run the application (Figure 6-40) you would find each thread has a unique ID (which is a good thing, as you should have two separate threads at this point).

Naming Threads

One interesting aspect of the Thread class is that it provides the ability to assign a friendly string name to the underlying path of execution. To do so, make use of the Name property. For example, you could update the MainClass as follows:

public class MainClass
{
     public static int Main(string[] args)
     {
          // Name the current thread.
          Thread primaryThread = Thread.CurrentThread;
          primaryThread.Name = "Boss man";

          Console.WriteLine("ID of  {0} is {1}", primaryThread.Name,
                                                 primaryThread.GetHashCode());

          // same code as before. . .
     }
}

The output is now as shown in Figure 6-41.

As you may be thinking, this property provides a more user-friendly way to identify the threads in your system.

Clogging Up the Primary Thread

The current application creates a secondary thread to perform a unit of work. The problem is the fact that printing 10 numbers takes no time at all, and therefore we are not really able to appreciate the fact that the primary thread is free to continue processing. Let's update the application in order to illustrate this very fact. First, let's update the WorkerClass to print out 30,000 numbers (using WriteLine() rather than Write() so you can see the print out) rather than a mere 10:

internal class WorkerClass
{
     public void DoSomeWork()
     {
          . . .
          // Do a lot of work.
          Console.Write("Worker says: ");
          for(int i = 0; i < 30000; i++)
          {
               Console.WriteLine(i + ", ");
          }
          Console.WriteLine();
     }
}

Next, let's update the MainClass such that it launches a message box directly after it creates the background worker thread:

public class MainClass
{
     public static int Main(string[] args)
     {
          // Name the current thread.
          . . .

          // Make worker class.
          . . .

          // Now make the thread.
          . . .
               
          // Now while background thread is working, 
          // do some additional work.
          MessageBox.Show("I'm busy");
 
          return 0;
     }
}

If you were to now run the application, you would see that the message box is displayed and can be moved around the desktop, while the background worker thread is busy pumping numbers to the console (Figure 6-42).

Now, contrast this behavior with what you might find if you had a single-threaded application. Assume the Main() method has been updated with logic that allows the user to enter the number of threads used within the AppDomain:

public static int Main(string[] args)
{
     Console.Write("Do you want [1] or [2] threads? ");
     string threadCount = Console.ReadLine();

     // Name the current thread.
     . . .

     // Make worker class.
     WorkerClass w = new WorkerClass();

     // Only make a new thread if the user said so.
     if(threadCount = = "2")
     {
          // Now make the thread.
          Thread backgroundThread = 
               new Thread(new ThreadStart(w.DoSomeWork));
          backgroundThread.Start();
     }
     else
          w.DoSomeWork();

     // Do some additional work.
     MessageBox.Show("I'm busy");
            
     return 0;
}

As you can guess, if the user enters the value "1" he or she must wait for all 30,000 numbers to be printed before seeing the message box appear, given that there is only a single thread in the AppDomain. However, if the user enters "2" he or she is able to interact with the message box while the secondary thread spins right along.

Putting a Thread to Sleep

The static Thread.Sleep() method can be used to currently suspend the current thread for a specified amount of time (specified in milliseconds). To illustrate, let's update the WorkerClass once again. This time around, the DoSomeWork() method does not print out 30,000 lines to the console, but 5 lines. The trick is, between each call to Console.WriteLine(), this background is put to sleep for approximately 5 seconds.

internal class WorkerClass
{
     public void DoSomeWork()
     {
          // Get some information about the worker thread.
          Console.WriteLine("ID of worker thread is: {0}",
                            Thread.CurrentThread.GetHashCode());

          // Do the work (and take a nap).
          Console.Write("Worker says: ");
          for(int i = 0; i < 5; i++)
          {
               Console.WriteLine(i + ", ");
               Thread.Sleep(5000);
          }
          Console.WriteLine();
     }
}

The output is shown in Figure 6-43.

Concurrency Revisited

Given this previous example, you might be thinking that threads are the magic bullet you have been looking for. Simply create threads for each part of your application and the end result will be increased application performance. You already know this is a loaded question, as the previous statement is false. If not used carefully and thoughtfully, too many threads can actually degrade an application's performance.

Even more important is the fact that each and every thread in a given AppDomain has direct access to the shared data of the application. In the current example, this is not a problem. However, imagine what might happen if the primary and secondary threads were both modifying a shared point of data. As you know, the thread scheduler will force threads to suspend their work at random. Since this is the case, what if thread A is kicked out of the way before it has fully completed its work? The answer is thread B is now reading unstable data.

To illustrate, let's build a new multithreaded C# Console Application named MultiThreadSharedData. This application also has a class named WorkerClass, which is functionally similar to the previous type of the same name:

internal class WorkerClass
{
     public void DoSomeWork()
     {
          // Do the work.
          for(int i = 0; i < 5; i++)
          {
               Console.WriteLine("Worker says: {0},", i);
          }
     }
}

You also have a type named MainClass. In this application, MainClass is responsible for creating three distinct secondary threads. The problem is that each of these threads is making calls to the shared instance of the WorkerClass type:

public class MainClass
{
     public static int Main(string[] args)
     {
          // Make the single worker object.
          WorkerClass w = new WorkerClass();

          // Create three secondary threads,
          // each of which makes calls to the same shared object.
          Thread workerThreadA = 
                 new Thread(new ThreadStart(w.DoSomeWork));
          Thread workerThreadB = 
                 new Thread(new ThreadStart(w.DoSomeWork));
          Thread workerThreadC = 
                 new Thread(new ThreadStart(w.DoSomeWork));

          // Now start each one.
          workerThreadA.Start();
          workerThreadB.Start();
          workerThreadC.Start();
          
          return 0;
     }
}

Now before you see some test runs, let's recap the problem. The primary thread of this AppDomain begins life by spawning three secondary worker threads. Each worker thread is told to make calls on the shared WorkerClass object instance. Given that we have taken no precautions to lock down this shared resource, the chances are very good that a given thread will be kicked out of the way before the WorkerClass is able to print out the results for the current thread. Because you don't know when this might happen, you are bound to get a number of strange results. For example, check out Figure 6-44.

Figure 6-45 shows another run.

And one more, just for good measure, appears in Figure 6-46.

Humm. There are clearly some problems. Given that each thread is telling the WorkerClass to "do some work" in a random way, the output is mangled (to say the least). What we need is a way to programmatically enforce synchronized access to the shared type. Like the Win32 API, the .NET base class libraries provide a number of synchronization techniques. Let's examine one possible approach.

C# "lock" Keyword

The first approach to providing synchronized access to our DoSomeWork() method is to make use of the C# lock statement. This intrinsic keyword allows you to lock down a block of code so that incoming threads must wait in line for the current thread to finish up its work. Using the lock statement is trivial:

internal class WorkerClass
{
     public void DoSomeWork()
     {
          // Only 1 thread at a time can tell the worker to get busy!
          lock(this)
          {
               // Do the work.
               for(int i = 0; i < 5; i++)
               {
                    Console.WriteLine("Worker says: {0},", i);
               }
          }
     }
}

If you rerun the application, you can see that the threads are instructed to politely wait in line for the current thread to finish its business Figure 6-47.

As you might guess, working with the C# lock statement is semantically equivalent to working with a raw Win32 CRITICAL_SECTION and related API function calls.

Using System.Threading.Monitor

The C# lock statement is really just a shorthand notation for working with the System.Threading.Monitor class type. Thus, if you were able to see what lock() actually resolves to under the hood, you would find the following:

internal class WorkerClass
{
     public void DoSomeWork()
     {
          // Define the item to monitor for synchronization. 
          Monitor.Enter(this);
          try
          {
               // Do the work.
               for(int i = 0; i < 5; i++)
               {
                    Console.WriteLine("Worker says: {0},", i);
               }
          }
          finally
          {
               // Error or not, you must exit the monitor.
               Monitor.Exit(this);
          }
     }
}

If you run the modified application, you would see no changes in the output (which is good). Here, we are making use of the static Enter() and Exit() members of the Monitor type, to enter (and leave) a locked block of code.

Using System.Threading.Interlocked

On a related note, the System.Threading namespace also provides a type that allows you to increment or decrement a variable by 1 in a thread-safe manner. To illustrate, assume that you have a class type (named IHaveNoIdea) which maintains an internal reference counter. One method of the class is responsible for incrementing this number by 1, while the other is responsible for decrementing this number by 1 (look familiar?):

public class IHaveNoIdea
{
     private long refCount = 0;

     public void AddRef()
     { ++refCount; }

     public void Release()
     {
          if(ÑrefCount = = 0)
          {
               GC.Collect();
          }
     }
}

If we have numerous threads of execution in the current AppDomain that are all making calls to AddRef() and Release(), the possibility exists that the internal refCount member variable could in fact have a value less that zero before the collection request can be posted to the garbage collector. Imagine threadA calls Release(), and is bumped out of the way by the thread scheduler just after the point at which it decremented the refCount. The next thread calling Release() would decrement the count again, at which point refCount is at currently at Ð1!

To prevent this behavior, you can make use of System.Threading.Interlocked, which atomically increments or decrements a given variable. Notice that a reference to the variable that is being modified is sent in, and thus you need to make use of the C# "ref" keyword:

public class IHaveNoIdea
{
     private long refCount = 0;

     public void AddRef()
     {
          Interlocked.Increment(ref refCount);
     }

     public void Release()
     {
          if(Interlocked.Decrement(ref refCount) = = 0)
          {
               GC.Collect();
          }
     }
}

At this point you have just enough information to become dangerous in the world of multithreaded assemblies. While this chapter does not dig into each and every aspect of the System.Threading namespace, you should be equipped to investigate additional details as you see fit.

Summary

This chapter drilled into the details behind the innocent looking .NET DLLs and EXEs located on your development machine. You began the journey by examining the core concepts of the assembly: metadata, manifests, and MSIL. Next, you contrasted shared and private assemblies, and investigated the steps taken by the assembly resolver to locate a given binary using application configuration files.

Assemblies are the building blocks of a .NET application. In essence, assemblies can be understood as binary units that contain some number of types that can be used by another application. As you have seen, assemblies may be private or shared. In stark contrast to classic COM, private assemblies are the default. When you wish to configure a shared assembly, you are making an explicit choice, and need to generate a corresponding strong name.

As you have also learned, the .NET framework defines the concept of an AppDomain. In many ways, AppDomains can be viewed as a lightweight process. Within a single AppDomain can exist any number of threads. Using the types defined within the System.Threading namespace, you are able to build thread-safe types that (as you have seen) can provide the end user with a more responsive application.

Copyright © 2001 APress

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.
This is a Organisation (No members)


Comments and Discussions

 
Generalwant key board shortcuts Pin
ssharmila5-May-07 21:35
ssharmila5-May-07 21:35 
GeneralAssembly versioning question Pin
marciawk6-Dec-03 22:21
marciawk6-Dec-03 22:21 
QuestionIs this a typo? Pin
Weidong Shen13-Dec-01 4:04
Weidong Shen13-Dec-01 4:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.