Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

364 C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

Exploring the ufo.netmodule File

Now, using ildasm.exe, open ufo.netmodule. As you can see, *.netmodules contain a module-level manifest; however, its sole purpose is to list each external assembly referenced by the code base. Given that the Ufo class did little more than make a call to Console.WriteLine(), you find the following:

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )

.ver 2:0:0:0

}

.module ufo.netmodule

Exploring the airvehicles.dll File

Next, using ildasm.exe, open the primary airvehicles.dll module and investigate the assembly-level manifest. Notice that the .file token documents the associated modules in the multifile assembly (ufo.netmodule in this case). The .class extern tokens are used to document the names of the external types referenced for use from the secondary module (Ufo):

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )

.ver 2:0:0:0

}

.assembly airvehicles

{

...

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.file ufo.netmodule

...

.class extern public AirVehicles.Ufo

{

.file ufo.netmodule

.class 0x02000002

}

.module airvehicles.dll

Again, realize that the only entity that links together airvehicles.dll and ufo.netmodule is the assembly manifest. These two binary files have not been merged into a single, larger *.dll.

Consuming a Multifile Assembly

The consumers of a multifile assembly couldn’t care less that the assembly they are referencing is composed of numerous modules. To keep things simple, let’s create a new Visual Basic .NET client application at the command line. Create a new file named Client.vb with the following Module definition. When you are done, save it in the same location as your multifile assembly.

Imports AirVehicles

Module Module1 Sub Main()

Dim h As New AirVehicles.Helicopter() h.TakeOff()

C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

365

' This will load the *.netmodule on demand.

Dim u As New UFO() u.AbductHuman()

End Sub End Module

To compile this executable assembly at the command line, you will make use of the Visual Basic

.NET command-line compiler, vbc.exe, with the following command set:

vbc /r:airvehicles.dll *.vb

Notice that when you are referencing a multifile assembly, the compiler needs to be supplied only with the name of the primary module (the *.netmodules are loaded on demand when used by the client’s code base). In and of themselves, *.netmodules do not have an individual version number and cannot be directly loaded by the CLR. Individual *.netmodules can be loaded only by the primary module (e.g., the file that contains the assembly manifest).

Note Visual Studio 2005 also allows you to reference a multifile assembly. Simply use the Add References dialog box and select the primary module. Any related *.netmodules are copied during the process.

At this point, you should feel comfortable with the process of building both single-file and multifile assemblies. To be completely honest, chances are that 99.99 percent of your assemblies will be single-file entities. Nevertheless, multifile assemblies can prove helpful when you wish to break a large physical binary into more modular units (and they are quite useful for remote download scenarios). Next up, let’s formalize the concept of a private assembly.

Source Code The MultifileAssembly project is included under the Chapter 11 subdirectory.

Understanding Private Assemblies

Technically speaking, the assemblies you’ve created thus far in this chapter have been deployed as private assemblies. Private assemblies are required to be located within the same directory as the client application (termed the application directory) or a subdirectory thereof. Recall that when you set a reference to CarLibrary.dll while building the CSharpCarClient.exe and VbNetCarClient.exe applications, Visual Studio 2005 responded by placing a copy of CarLibrary.dll within the client’s application directory.

When a client program uses the types defined within this external assembly, the CLR simply loads the local copy of CarLibrary.dll. Because the .NET runtime does not consult the system registry when searching for referenced assemblies, you can relocate the CSharpCarClient.exe (or VbNetCarClient.exe) and CarLibrary.dll assemblies to location on your machine and run the application (this is often termed Xcopy deployment).

Uninstalling (or replicating) an application that makes exclusive use of private assemblies is a no-brainer: simply delete (or copy) the application folder. Unlike with COM applications, 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.

The Identity of a Private Assembly

The full identity of a private assembly consists of the friendly name and numerical version, both of which are recorded in the assembly manifest. The friendly name simply is the name of the module

366C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

that contains the assembly’s manifest minus the file extension. For example, if you examine the manifest of the CarLibrary.dll assembly, you find the following (your version will no doubt differ):

.assembly CarLibrary

{

...

.ver 1:0:454:30104

}

Given the isolated nature of a private assembly, it should make sense that the CLR does not bother to make use of the version number when resolving its location. The assumption is that private assemblies do not need to have any elaborate version checking, as the client application is the only entity that “knows” of its existence. Given this, it is (very) possible for a single machine to have multiple copies of the same private assembly in various application directories.

Understanding the Probing Process

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 request to the location of the requested binary file. Strictly speaking, a request to load an assembly may be either implicit or explicit. An implicit load request occurs when the CLR consults the manifest in order to resolve the location of an assembly defined using the .assembly extern tokens:

// An implicit load request.

.assembly extern CarLibrary

{ ...}

An explicit load request occurs programmatically using the Load() or LoadFrom() method of the System.Reflection.Assembly class type, typically for the purposes of late binding and dynamic invocation of type members. You’ll examine these topics further in Chapter 12, but for now you can see an example of an explicit load request in the following code:

// An explicit load request.

Assembly asm = Assembly.Load("CarLibrary");

In either case, the CLR extracts the friendly name of the assembly and begins probing the client’s application directory for a file named CarLibrary.dll. If this file cannot be located, an attempt is made to locate an executable assembly based on the same friendly name (CarLibrary.exe). If neither of these files can be located in the application directory, the runtime gives up and throws a FileNotFound exception at runtime.

Note Technically speaking, if a copy of the requested assembly cannot be found within the client’s application directory, the CLR will also attempt to locate a client subdirectory with the exact same name as the assembly’s friendly name (e.g., C:\MyClient\CarLibrary). If the requested assembly resides within this subdirectory, the CLR will load the assembly into memory.

Configuring Private Assemblies

While it is possible to deploy a .NET application by simply copying all required assemblies to a single folder on the user’s hard drive, you will most likely wish to define a number of subdirectories to group related content. For example, assume you have an application directory named C:\MyApp that contains CSharpCarClient.exe. Under this folder might be a subfolder named MyLibraries that contains CarLibrary.dll.

C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

367

Regardless of the intended relationship between these two directories, the CLR will not probe the MyLibraries subdirectory unless you supply a configuration file. Configuration files contain various XML elements that allow you to influence the probing process. By “law,” configuration files must have the same name as the launching application and take a *.config file extension, and they must be deployed in the client’s application directory. Thus, if you wish to create a configuration file for CSharpCarClient.exe, it must be named CSharpCarClient.exe.config.

To illustrate the process, create a new directory on your C drive named MyApp using Windows Explorer. Next, copy CSharpCarClient.exe and CarLibrary.dll to this new folder, and run the program by double-clicking the executable. Your program should run successfully at this point (remember, the assemblies are not registered!). Next, create a new subdirectory under C:\MyApp named MyLibraries (see Figure 11-11), and move CarLibrary.dll to this location.

Figure 11-11. CarLibrary.dll now resides under the MyLibraries subdirectory.

Try to run your client program again. Because the CLR could not locate “CarLibrary” directly within the application directory, you are presented with a rather nasty unhandled FileNotFound exception.

To rectify the situation, create a new configuration file named CSharpCarClient.exe.config and save it in the same folder containing the CSharpCarClient.exe application, which in this example would be C:\MyApp. Open this file and enter the following content exactly as shown (be aware that XML is case sensitive!):

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="MyLibraries"/>

</assemblyBinding>

</runtime>

</configuration>

.NET *.config files always open with a root element named <configuration>. The nested <runtime> element may specify an <assemblyBinding> element, which nests a further element named <probing>. The privatePath attribute is the key point in this example, as it is used to specify the subdirectories relative to the application directory where the CLR should probe.

368 C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

Do note that the <probing> element does not specify which assembly is located under a given subdirectory. In other words, you cannot say, “CarLibrary is located under the MyLibraries subdirectory, but MathUtils is located under Bin subdirectory.” The <probing> element simply instructs the CLR to investigate all specified subdirectories for the requested assembly until the first match is encountered.

Note Be very aware that the privatePath attribute cannot be used to specify an absolute (C:\SomeFolder\ SomeSubFolder) or relative (..\\SomeFolder\\AnotherFolder) path! If you wish to specify a directory outside the client’s application directory, you will need to make use of a completely different XML element named <codeBase> (more details on this element later in the chapter).

Multiple subdirectories can be assigned to the privatePath attribute using a semicolon-delimited list. You have no need to do so at this time, but here is an example that informs the CLR to consult the MyLibraries and MyLibraries\Tests client subdirectories:

<probing privatePath="MyLibraries; MyLibraries\Tests"/>

Once you’ve finished creating CSharpCarClient.exe.config, run the client by double-clicking the executable in Windows Explorer. You should find that CSharpCarClient.exe executes without a hitch (if this is not the case, double-check it for typos).

Next, for testing purposes, change the name of your configuration file (in one way or another) and attempt to run the program once again. The client application should now fail. Remember that *.config files must be prefixed with the same name as the related client application. By way of a final test, open your configuration file for editing and capitalize any of the XML elements. Once the file is saved, your client should fail to run once again (as XML is case sensitive).

Configuration Files and Visual Studio 2005

While you are always able to create XML configuration files by hand using your text editor of choice, Visual Studio 2005 allows you create a configuration file during the development of the client program. To illustrate, load the CSharpCarClient solution into Visual Studio 2005 and insert a new Application Configuration File item using the Project Add New Item menu selection. Before you click the OK button, take note that the file is named App.config (don’t rename it!). If you look in the Solution Explorer window, you will now find App.config has been inserted into your current project (see Figure 11-12).

Figure 11-12. The Visual Studio 2005 App.config file

At this point, you are free to enter the necessary XML elements for the client you happen to be creating. Now, here is the cool thing. Each time you compile your project, Visual Studio 2005 will

C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

369

automatically copy the data in App.config to the \Bin\Debug directory using the proper naming convention (such as CSharpCarClient.exe.config). However, this behavior will happen only if your configuration file is indeed named App.config.

Using this approach, all you need to do is maintain App.config, and Visual Studio 2005 will ensure your application directory contains the latest and greatest content (even if you happen to rename your project).

Introducing the .NET Framework 2.0 Configuration Utility

Although authoring a *.config file by hand is not too traumatic, the .NET Framework 2.0 SDK does ship with a tool that allows you to build XML configuration files using a friendly GUI. You can find the .NET Framework 2.0 Configuration utility under the Administrative folder of your Control Panel. Once you launch this tool, you will find a number of configuration options (see Figure 11-13).

Figure 11-13. The .NET Framework 2.0 Configuration utility

To build a client *.config file using this utility, your first step is to add the application to configure by right-clicking the Applications node and selecting Add. In the resulting dialog box, you may find the application you wish to configure, provided that you have executed it using Windows Explorer. If this is not the case, click the Other button and navigate to the location of the client program you wish to configure. For this example, select the VbNetCarClient.exe application created earlier in this chapter (look under the Bin folder). Once you have done so, you will now find a new subnode, as shown in Figure 11-14.

370 C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

Figure 11-14. Preparing to configure VbNetCarClient.exe

If you right-click the VbNetCarClient node and activate the Properties page, you will notice a text field located at the bottom of the dialog box where you can enter the values to be assigned to the privatePath attribute. Just for testing purposes, enter a subdirectory named TestDir (see Figure 11-15).

Figure 11-15. Configuring a private probing path graphically

Once you click the OK button, you can examine the VbNetCarClient\Debug directory and find that the default *.config file (which Visual Studio 2005 provides for most VB .NET programs) has been updated with the correct <probing> element.

C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

371

Note As you may guess, you can copy the XML content generated by the .NET Framework 2.0 Configuration utility into a Visual Studio 2005 App.config file for further editing. Using this approach, you can certainly decrease your typing burden by allowing the tool to generate the initial content.

Understanding Shared Assemblies

Now that you understand how to deploy and configure a private assembly, you can begin to examine the role of a shared assembly. Like a private assembly, a shared assembly is a collection of types and (optional) resources. The most obvious difference between shared and private assemblies is the fact that a single copy of a shared assembly can be used by several applications on a single machine.

Consider all the applications created in this text that required you to set a reference to System. Windows.Forms.dll. If you were to look in the application directory of each of these clients, you would not find a private copy of this .NET assembly. The reason is that System.Windows.Forms.dll has been deployed as a shared assembly. Clearly, if you need to create a machine-wide class library, this is the way to go.

As suggested in the previous paragraph, a shared assembly is not deployed within the same directory as the application making use of it. Rather, shared assemblies are installed into the Global Assembly Cache (GAC). The GAC is located under a subdirectory of your Windows directory named Assembly (e.g., C:\Windows\Assembly), as shown in Figure 11-16.

Figure 11-16. The GAC

Note You cannot install executable assemblies (*.exe) into the GAC. Only assemblies that take the *.dll file extension can be deployed as a shared assembly.

Understanding Strong Names

Before you can deploy an assembly to the GAC, you must assign it a strong name, which is used to uniquely identify the publisher of a given .NET binary. Understand that a “publisher” could be an individual programmer, a department within a given company, or an entire company at large.

372 C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

In some ways, a strong name is the modern day .NET equivalent of the COM globally unique identifier (GUID) identification scheme. If you have a COM background, you may recall that AppIDs are GUIDs that identify a particular COM application. Unlike COM GUID values (which are nothing more than 128-bit numbers), strong names are based (in part) on two cryptographically related keys (termed the public key and the private key), which are much more unique and resistant to tampering than a simple GUID.

Formally, a strong name is composed of a set of related data, much of which is specified using assembly-level attributes:

The friendly name of the assembly (which you recall is the name of the assembly minus the file extension)

The version number of the assembly (assigned using the [AssemblyVersion] attribute)

The public key value (assigned using the [AssemblyKeyFile] attribute)

An optional culture identity value for localization purposes (assigned using the [AssemblyCulture] attribute)

An embedded digital signature created using a hash of the assembly’s contents and the private key value

To provide a strong name for an assembly, your first step is to generate public/private key data using the .NET Framework 2.0 SDK’s sn.exe utility (which you’ll do momentarily). The sn.exe utility responds by generating a file (typically ending with the *.snk [Strong Name Key] file extension) that contains data for two distinct but mathematically related keys, the “public” key and the “private” key. Once the C# compiler is made aware of the location for your *.snk file, it will record the full public key value in the assembly manifest using the .publickey token at the time of compilation.

The C# compiler will also generate a hash code based on the contents of the entire assembly (CIL code, metadata, and so forth). As you recall from Chapter 3, a hash code is a numerical value that is unique for a fixed input. Thus, if you modify any aspect of a .NET assembly (even a single character in a string literal) the compiler yields a unique hash code. This hash code is combined with the private key data within the *.snk file to yield a digital signature embedded within the assembly’s CLR header data. The process of strongly naming an assembly is illustrated in Figure 11-17.

Figure 11-17. At compile time, a digital signature is generated and embedded into the assembly based in part on public and private key data.

C H A P T E R 1 1 I N T R O D U C I N G . N E T A S S E M B L I E S

373

Understand that the actual private key data is not listed anywhere within the manifest, but is used only to digitally sign the contents of the assembly (in conjunction with the generated hash code). Again, the whole idea of making use of public/private key cryptography is to ensure that no two companies, departments, or individuals have the same identity in the .NET universe. In any case, once the process of assigning a strong name is complete, the assembly may be installed into the GAC.

Note Strong names also provide a level of protection against potential evildoers tampering with your assembly’s contents. Given this point, it considered a .NET best practice to strongly name every assembly regardless if it is deployed to the GAC.

Strongly Naming CarLibrary.dll

Let’s walk through the process of assigning a strong name to the CarLibrary assembly created earlier in this chapter (go ahead and open up that project using your IDE of choice). The first order of business is to generate the required key data using the sn.exe utility. Although this tool has numerous command-line options, all you need to concern yourself with for the moment is the -k flag, which instructs the tool to generate a new file containing the public/private key information. Create a new folder on your C drive named MyTestKeyPair and change to that directory using the .NET Command Prompt. Now, issue the following command to generate a file named MyTestKeyPair.snk:

sn -k MyTestKeyPair.snk

Now that you have your key data, you need to inform the C# compiler exactly where MyTestKeyPair.snk is located. When you create any new C# project workspace using Visual Studio 2005, you will notice that one of your initial project files (located under the Properties node of Solution Explorer) is named AssemblyInfo.cs. This file contains a number of attributes that describe the assembly itself. The AssemblyKeyFile assembly-level attribute can be used to inform the compiler of the location of a valid *.snk file. Simply specify the path as a string parameter, for example:

[assembly: AssemblyKeyFile(@"C:\MyTestKeyPair\MyTestKeyPair.snk")]

Given that the version of a shared assembly is one aspect of a strong name, let’s also specify a specific version number for CarLibrary.dll. In the AssemblyInfo.cs file, you will find another attribute named AssemblyVersion. Initially the value is set to 1.0.*:

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

Recall that a .NET version number is composed of the four parts (<major>.<minor>.<build>. <revision>). Until you say otherwise, Visual Studio 2005 automatically increments the build and revision numbers (as marked by the * wildcard token) as part of each compilation. To enforce a fixed value for the assembly’s build version, replace the wildcard token with a specific build and revision value:

//Format: <Major version>.<Minor version>.<Build number>.<Revision>

//Valid values for each part of the version number are between 0 and 65535.

[assembly: AssemblyVersion("1.0.0.0")]

At this point, the C# compiler has all the information needed to generate strong name data (as you are not specifying a unique culture value via the [AssemblyCulture] attribute, you “inherit” the culture of your current machine). Compile your CarLibrary code library and open the manifest using ildasm.exe. You will now see a new .publickey tag is used to document the full public key information, while the .ver token records the version specified via the [AssemblyVersion] attribute (see Figure 11-18).