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.
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.
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.
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.
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.
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.
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:
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.
Build your assembly with an assembly version number and the key information from originator.key.
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.
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.
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]
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.
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.
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]
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.
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.
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.
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.