Chapter 4. Working with .NET Components

Having seen the language-integration examples in the previous chapter, we know that all .NET assemblies are essentially binary components.[20] You can treat each .NET assembly as a component that you can plug into another component or application, without the need for source code, since all the metadata for the component is stored inside the .NET assembly. While you have to perform a ton of plumbing to build a component in COM, you need to perform zero extra work to get a component in .NET, as all .NET assemblies are components by nature.

In this chapter, we examine the more advanced topics, including component deployment, distributed components, and enterprise services, such as transaction management, object pooling, role-based security, and message queuing.

Deployment Options

For a simple program like hello.exe that we built in Chapter 2, deployment is easy: copy the assembly into a directory, and it’s ready to run. When you want to uninstall it, remove the file from the directory. However, when you want to share components with other applications, you’ve got to do some work.

In COM, you must store activation and marshaling[21] information in the registry for components to interoperate; as a result, any COM developer can discuss at length the pain and suffering inherent in COM and the system registry. In .NET, the system registry is no longer necessary for component integration.

In the .NET environment, components can be private, meaning that they are unpublished and used by known clients, or shared , meaning that they are published and used by all clients. This section discusses several options for deploying private and shared components.

Private Components

If you have private components that are used only by specific clients, you have two deployment options. You can store the private components and the clients that use these components in the same directory, or you can store the components in a specific directory that the client can access. Since these clients use the exact private components that they referenced at build time, the CLR doesn’t support version checking or enforce version policies on private components.

To install your applications in either of these cases, perform a simple xcopy of your application files from the source installation directory to the destination directory. When you want to remove the application, remove these directories. You don’t have to write code to store information into the registry, so there’s no worrying about whether you’ve missed inserting a registry setting for correct application execution. In addition, because nothing is stored in the registry, you don’t have to worry about registry residues.

One-directory deployment

To specify component location in the same directory as the client application, use the following syntax (as we did in a Chapter 3 example):

csc /r:vehicle.dll;car.dll;plane.dll /t:exe /out:drive.exe drive.cs

The reference to plane.dll does not include a directory path; therefore, the C# compiler stores this reference as is into the client application’s assembly manifest so that the CLR can resolve this reference at runtime (i.e., find and load plane.dll and activate the Plane class). If you move any of the DLLs to a different directory, you will get an exception when you execute drive.exe.

Multiple-directory deployment

Instead of storing all components in the same directory as your client application, you can also use multiple, private paths to segregate your components to be easier to find and manage. For example, we will separate the vehicle, car, and plane components into their own private directories, as shown in Figure 4-1. We will leave the drive.exe application in the top directory, MultiDirectories.

Multiple-directory tree of components

Figure 4-1. Multiple-directory tree of components

When you build the vehicle component, you don’t have to do anything special, as it doesn’t reference or use any third-party components. However, when you build the car or plane component, you must refer to the correct vehicle component (i.e., the one in the vehicle directory). For example, to build the plane component successfully, you must explicitly refer to vehicle.dll using a specific or relative path, as shown in the following command (cd to the plane directory):

csc/r:..\vehicle\vehicle.dll /t:library /out:plane.dll plane.cs

You can build the car component the same way you build the plane component. To compile your client application, you must also refer to your dependencies using the correct paths (cd to the main directory, MultiDirectories, before you type this command all on one line):

csc/r:vehicle\vehicle.dll;car\car.dll;plane\plane.dll 
    /t:exe /out:drive.exe drive.cs

When you execute this command, the C# compiler records these referenced private paths into your application’s assembly manifest. When you execute drive.exe, the CLR looks into your application’s assembly manifest to find and load the target components.

Shared Components

Unlike application-private assemblies, shared assemblies—ones that can be used by any client application—must be published or registered in the system Global Assembly Cache (GAC). When you register your assemblies against the GAC, they act as system components, such as a system DLL that every process in the system can use. A prerequisite for GAC registration is that the component must possess originator and version information. In addition to other metadata, these two items allow multiple versions of the same component to be registered and executed on the same machine. Again, unlike COM, we don’t have to store any information in the system registry for clients to use these shared assemblies.

There are three general steps to registering your shared assemblies against the GAC:

  1. Use the shared named (sn.exe) utility to obtain a public/private key pair. This utility generates a random key pair for you and saves the key information in an output file—for example, originator.key.

  2. Build your assembly with an assembly version number and the key information from originator.key.

  3. Use the .NET Global Assembly Cache Utility (gactutil.exe) to register your assembly in the GAC. This assembly is now a shared assembly and can be used by any client.

The commands that we use in this section refer to relative paths, so if you’re following along, make sure that you create the directory structure as shown in Figure 4-2. The vehicle, plane, and car directories hold their appropriate assemblies, and the key directory holds the public/private key pair that we will generate in a moment. The car-build directory holds a car assembly with a modified build number, and the car-revision directory holds a car assembly with a modified revision number.

Directory structure for examples in this section

Figure 4-2. Directory structure for examples in this section

Generating a random key pair

We will perform the first step once and reuse the key pair for all shared assemblies that we build in this section. We’re doing this for brevity because you can use different key information for each assembly, or even each version, that you build. Here’s how to generate a random key pair (be sure to do this in the key directory):

sn -k originator.key

The -k option generates a random key pair and saves the key information into the originator.key file. We will use this file as input when we build our shared assemblies. Let’s now examine steps 2 and 3 of registering your shared assemblies against the GAC.

Making the vehicle component a shared assembly

In order to add version and key information into the vehicle component (developed using Managed C++), we need to make some minor modifications to vehicle.cpp, as follows:

#using<mscorlib.dll>
using namespace System;using namespace System::Reflection;
                  [assembly:AssemblyVersion("1.0.0.0")];
                  [assembly:AssemblyKeyFile("..\\key\\originator.key")]; 

public _     _gc _     _interface ISteering
{
  void TurnLeft(  );
  void TurnRight(  );
};

public _     _gc class Vehicle : public ISteering  
{
  public:

    virtual void TurnLeft(  )
    {
      Console::WriteLine("Vehicle turns left."); 
    }

    virtual void TurnRight(  )
    {
      Console::WriteLine("Vehicle turn right."); 
    }

    virtual void ApplyBrakes(  ) = 0; 
};

The first boldface line indicates that we’re using the Reflection namespace, which defines the attributes that the compiler will intercept to inject the correct information into our assembly manifest. (For a discussion of attributes, see Section 4.3.1 later in this chapter.) We use the AssemblyVersion attribute to indicate the version of this assembly, and we use the AssemblyKeyFile attribute to indicate the file containing the key information that the compiler should use to derive the public-key-token value.

Once you’ve done this, you can build this assembly using the following commands, which you’ve seen before:

cl /CLR /c vehicle.cpp
link -dll /out:vehicle.dll -noentry vehicle.obj

After you’ve built the assembly, you can use the .NET Global Assembly Cache Utility to register this assembly into the GAC, as follows:

gacutil.exe /i vehicle.dll

Successful registration against the cache turns this component into a shared assembly. A version of this component is copied into the GAC so that even if you delete this file locally, you will still be able to run your client program.[22]

Making the car component a shared assembly

In order to add version and key information into the car component, we need to make some minor modifications to car.vb, as follows:

Imports SystemImports System.Reflection
                  <Assembly:AssemblyVersion("1.0.0.0")>
                  <assembly:AssemblyKeyFile("..\\key\\originator.key")>

Public Class Car
  Inherits Vehicle

  Overrides Public Sub TurnLeft(  )
    Console.WriteLine("Car turns left.")
  End Sub

  Overrides Public Sub TurnRight(  )
    Console.WriteLine("Car turns right.")
  End Sub

  Overrides Public Sub ApplyBrakes(  )
    Console.WriteLine("Car trying to stop.")Console.WriteLine("ORIGINAL VERSION - 1.0.0.0.")
    throw new Exception("Brake failure!")
  End Sub

End Class

Having done this, you can now build it with the following command:

vbc /r:..\vehicle\vehicle.dll /t:library /out:car.dll car.vb

Notice that the car component uses a specific vehicle component, ..\vehicle\vehicle.dll. At runtime, if the CLR cannot find this specific file here or within the GAC, it will throw an exception. Once you’ve built this component, you can register it against the GAC:

gacutil /i car.dll

At this point, you can delete car.dll in the local directory because it has been registered in the GAC.

Making the plane component a shared assembly

In order to add version and key information into the plane component, we need to make some minor modifications to plane.cs, as follows:

using System;using System.Reflection;
                  [assembly:AssemblyVersion("1.0.0.0")]
                  [assembly:AssemblyKeyFile("..\\key\\originator.key")]

public class Plane : Vehicle 
{
  override public void TurnLeft(  ) 
  {
    Console.WriteLine("Plane turns left.");
  }

  override public void TurnRight(  )
  {
    Console.WriteLine("Plane turns right.");
  }

  override public void ApplyBrakes(  )
  {
    Console.WriteLine("Air brakes being used.");
  }
}

Having done this, you can build the assembly with the following command:

csc /r:..\vehicle\vehicle.dll /t:library /out:plane.dll plane.cs 

gacutil /i plane.dll

Of course, the last line in this snippet simply registers the component into the GAC.

Viewing the GAC

Now that we’ve registered all our components into the GAC, let’s see what the GAC looks like. Microsoft has shipped a shell extension, the Shell Cache Viewer, to make it easier for you to view the GAC. On our machines, the Shell Cache Viewer appears when we navigate to C:\WINNT\Assembly, as shown in Figure 4-3.[23]

Our shared assemblies in the GAC

Figure 4-3. Our shared assemblies in the GAC

As you can see, the Shell Cache Viewer shows that all our components have the same version number because we used 1.0.0.0 as the version number when we built our components. Additionally, it shows all our components having the same public-key-token value, because we used the same key file, originator.key.

Building and testing the drive.exe

You should copy the previous drive.cs source-code file into the Shared Assemblies directory, the root of the directory structure (shown in Figure 4-2) we are working with in this section. Having done this, you can build this component as follows (remember to type everything on one line):

csc /r:vehicle\vehicle.dll;car\car.dll;plane\plane.dll 
    /t:exe /out:drive.exe drive.cs

Once you’ve done this, you can execute the drive.exe component, which will use the vehicle.dll, car.dll, and plane.dll assemblies registered in the GAC. You should see the following as part of your output:

ORIGINAL VERSION - 1.0.0.0.

To uninstall these shared components, select the appropriate assemblies and press the Delete key (but if you do this now, you must reregister these assemblies because we’ll need them in the upcoming examples). When you do this, you’ve taken all the residues of these components out of the GAC. All that’s left is to delete any files that you’ve copied over from your installation diskette—typically, all you really have to do is recursively remove the application directory.

Adding new versions

Unlike private assemblies, shared assemblies can take advantage of the rich versioning policies that the CLR supports. Unlike earlier OS-level infrastructures, the CLR enforces versioning policies during the loading of all shared assemblies. By default, the CLR loads the assembly with which your application was built, but by providing an application configuration file, you can command the CLR to load the specific assembly version that your application needs. Inside an application configuration file, you can specify the rules or policies that the CLR should use when loading shared assemblies upon which your application depends.

Let’s make some code changes to our car component to demonstrate the default versioning support. Remember that version 1.0.0.0 of our car component’s ApplyBrakes( ) method throws an exception, as follows:

Overrides Public Sub ApplyBrakes(  )
  Console.WriteLine("Car trying to stop.")Console.WriteLine("ORIGINAL VERSION - 1.0.0.0.")
  throw new Exception("Brake failure!")
End Sub

Let’s create a different build to remove this exception. To do this, make the following changes to the ApplyBrakes( ) method (store this source file in the car-build directory):

Overrides Public Sub ApplyBrakes(  )
  Console.WriteLine("Car trying to stop.")Console.WriteLine("BUILD NUMBER change - 1.0.1.0.")
End Sub

In addition, you need to change the build number in your code as follows:

<Assembly:AssemblyVersion("1.0.1.0")>

Now build this component, and register it using the following commands:

vbc /r:..\vehicle\vehicle.dll 
    /t:library /out:car.dll car.vb 
gacutil /i car.dll

Notice that we’ve specified that this version is 1.0.1.0, meaning that it’s compatible with Version 1.0.0.0. After registering this assembly with the GAC, execute your drive.exe application, and you will see the following statement as part of the output:

ORIGINAL VERSION - 1.0.0.0.

This is the default behavior—the CLR will load the version of the assembly with which your application was built. And just to prove this statement further, suppose that you provide Version 1.0.1.1 by making the following code changes (store this version in the car-revision directory):

Overrides Public Sub ApplyBrakes(  )
  Console.WriteLine("Car trying to stop.")Console.WriteLine("REVISION NUMBER change - 1.0.1.1.")
End Sub
<Assembly:AssemblyVersion("1.0.1.1")>

This time, instead of changing the build number, you’re changing the revision number, which should still be compatible to the previous two versions. If you build this assembly, register it against the GAC, and execute drive.exe again, you will get the following statement as part of your output:

ORIGINAL VERSION - 1.0.0.0.

Again, the CLR chooses the version with which your application was built.

As shown in Figure 4-4, you can use the Shell Cache Viewer to verify that all three versions exist on the system simultaneously. This implies that the support exists for side-by-side execution—which terminated DLL Hell in .NET.

Multiple versions of the same shared assembly

Figure 4-4. Multiple versions of the same shared assembly

If you want your program to use a different compatible version of the car assembly, you have to provide an application configuration file. The name of an application configuration file is composed of the physical executable name and “.config” appended to it. For example, since our client program is named drive.exe, its configuration file must be named drive.exe.config.

Here’s a drive.exe.config file that allows you to tell the CLR to load Version 1.0.1.0 of the car assembly for you (instead of loading the default version, 1.0.0.0). The two boldface attributes say that although we built our client with version 1.0.0.0 (oldVersion) of the car assembly, load 1.0.1.0 (newVersion) for us when we run drive.exe.

<?xml version ="1.0"?>
<configuration>   
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

      <dependentAssembly>
        <assemblyIdentity name="car" 
           publicKeyToken="D730D98B6BDE2BBA"
           culture="" />
                
        <bindingRedirectoldVersion="1.0.0.0"
                         newVersion="1.0.1.0" />

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

Once you create this configuration file (stored in the same directory as the drive.exe executable) and execute drive.exe, you will see the following as part of your output:

BUILD NUMBER change - 1.0.1.0.

If you change the configuration file so that newVersion=1.0.1.1 and if you execute drive.exe again, you will see the following as part of your output:

REVISION NUMBER change - 1.0.1.1.

There are two other attributes in this configuration file that we want to explain. The name attribute of the assemblyIdentity tag indicates the shared assembly’s human-readable name that is stored in the GAC. The publicKeyToken attribute records the public-key-token value, which is an 8-byte hash of the public key used to build this component. There are several ways to get this 8-byte hash: you can copy it from the Shell Cache Viewer, you can copy it from the IL dump of your component, or you can use the Shared Name utility to get it, as follows:

sn -T car.dll

Having gone over all these examples, you should realize that you have full control over which dependent assembly versions the CLR should load for your applications. It doesn’t matter which version was built with your application: you can choose different versions at runtime merely by changing a few attributes in the application configuration file.



[20] Remember, as we explained in Chapter 1, we’re using the term “component” as a binary, deployable unit, not as a COM class.

[21] Distributed application requires a communication layer to assemble and disassemble application data and network streams. This layer is formally known as a marshaler in Microsoft terminology. Assembling and disassembling an application-level protocol network buffer are formally known as marshaling and unmarshaling, respectively.

[22] However, don’t delete the file now because we need it to build the car and plane assemblies.

[23] This path is entirely dependent upon the %windir% setting on your machine.

Get .Net Framework Essentials now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.