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

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

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

384 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-28. Behold! The GAC’s internal copy of CarLibrary.dll.

When you install a strongly named assembly into the GAC, the operating system responds by extending the directory structure beneath the Assembly subdirectory. Using this approach, the CLR is able to manipulate multiple versions of a specific assembly while avoiding the expected name clashes resulting from identically named *.dlls.

Understanding Publisher Policy Assemblies

The next configuration issue you’ll examine is the role of publisher policy assemblies. As you’ve just seen, *.config files can be constructed to bind to a specific version of a shared assembly, thereby bypassing the version recorded in the client manifest. While this is all well and good, imagine you’re an administrator who now needs to reconfigure all client applications on a given machine to rebind to version 2.0.0.0 of the CarLibrary.dll assembly. Given the strict naming convention of a configuration file, you would need to duplicate the same XML content in numerous locations (assuming you are, in fact, aware of the locations of the executables using CarLibrary!). Clearly this would be a maintenance nightmare.

Publisher policy allows the publisher of a given assembly (you, your department, your company, or what have you) to ship a binary version of a *.config file that is installed into the GAC along with the newest version of the associated assembly. The benefit of this approach is that client application directories do not need to contain specific *.config files. Rather, the CLR will read the current manifest and attempt to find the requested version in the GAC. However, if the CLR finds a publisher policy assembly, it will read the embedded XML data and perform the requested redirection at the level of the GAC.

Publisher policy assemblies are created at the command line using a .NET utility named al.exe (the assembly linker). While this tool provides a large number of options, building a publisher policy assembly requires you only to pass in the following input parameters:

The location of the *.config or *.xml file containing the redirecting instructions

The name of the resulting publisher policy assembly

The location of the *.snk file used to sign the publisher policy assembly

The version numbers to assign the publisher policy assembly being constructed

If you wish to build a publisher policy assembly that controls CarLibrary.dll, the command set is as follows:

al /link: CarLibraryPolicy.xml /out:policy.1.0.CarLibrary.dll /keyf:C:\MyKey\myKey.snk /v:1.0.0.0

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

385

Here, the XML content is contained within a file named CarLibraryPolicy.xml. The name of the output file (which must be in the format policy.<major>.<minor>.assemblyToConfigure) is specified using the obvious /out flag. In addition, note that the name of the file containing the public/private key pair will also need to be supplied via the /keyf option. (Remember, publisher policy files are shared, and therefore must have a strong name!)

Once the al.exe tool has executed, the result is a new assembly that can be placed into the GAC to force all clients to bind to version 2.0.0.0 of CarLibrary.dll, without the use of a specific client application configuration file.

Disabling Publisher Policy

Now, assume you (as a system administrator) have deployed a publisher policy assembly (and the latest version of the related assembly) to a client machine. As luck would have it, nine of the ten affected applications rebind to version 2.0.0.0 without error. However, the remaining client application (for whatever reason) blows up when accessing CarLibrary.dll 2.0.0.0 (as we all know, it is next to impossible to build backward-compatible software that works 100 percent of the time).

In such a case, it is possible to build a configuration file for a specific troubled client that instructs the CLR to ignore the presence of any publisher policy files installed in the GAC. The remaining client applications that are happy to consume the newest .NET assembly will simply be redirected via the installed publisher policy assembly. To disable publisher policy on a client-by-client basis, author a (properly named) *.config file that makes use of the <publisherPolicy> element and set the apply attribute to no. When you do so, the CLR will load the version of the assembly originally listed in the client’s manifest.

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<publisherPolicy apply="no" />

</assemblyBinding>

</runtime>

</configuration>

Understanding the <codeBase> Element

Application configuration files can also specify code bases. The <codeBase> element can be used to instruct the CLR to probe for dependent assemblies located at arbitrary locations (such as network share points, or simply a local directory outside a client’s application directory).

Note If the value assigned to a <codeBase> element is located on a remote machine, the assembly will be downloaded on demand to a specific directory in the GAC termed the download cache. You can view the content of your machine’s download cache by supplying the /ldl option to gacutil.exe.

Given what you have learned about deploying assemblies to the GAC, it should make sense that assemblies loaded from a <codeBase> element will need to be assigned a strong name (after all, how else could the CLR install remote assemblies to the GAC?).

Note Technically speaking, the <codeBase> element can be used to probe for assemblies that do not have

a strong name. However, the assembly’s location must be relative to the client’s application directory (and thus is little more than an alternative to the <privatePath> element).

386 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

Create a console application named CodeBaseClient, set a reference to CarLibrary.dll version 2.0.0.0, and update the initial file as so:

using CarLibrary;

namespace CodeBaseClient

{

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with CodeBases *****");

SportsCar c = new SportsCar(); Console.WriteLine("Sports car has been allocated."); Console.ReadLine();

}

}

}

Given that CarLibrary.dll has been deployed to the GAC, you are able to run the program as is. However, to illustrate the use of the <codeBase> element, create a new folder under your C drive (perhaps C:\MyAsms) and place a copy of CarLibrary.dll version 2.0.0.0 into this directory.

Now, add an App.config file to the CodeBaseClient project (as explained earlier in this chapter) and author the following XML content (remember that your .publickeytoken value will differ; consult your GAC as required):

<configuration>

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly>

<assemblyIdentity name="SharedAssembly" publicKeyToken="219ef380c9348a38" />

<codeBase version="2.0.0.0" href="file:///C:\MyAsms\CarLibrary.dll" />

</dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

As you can see, the <codeBase> element is nested within the <assemblyIdentity> element, which makes use of the name and publicKeyToken attributes to specify the friendly name as associated publicKeyToken values. The <codeBase> element itself specifies the version and location (via the href property) of the assembly to load. If you were to delete version 2.0.0.0 of CarLibrary.dll from the GAC, this client would still run successfully, as the CLR is able to resolve locate the external assembly under C:\MyAsms.

However, if you were to delete the MyAsms directory from your machine, the client would now fail. Clearly the <codeBase> elements (if present) take precedence over the investigation of the GAC.

Note If you place assemblies at random locations on your development machine, you are in effect re-creating the system registry (and the related DLL hell), given that if you move or rename the folder containing your binaries, the current bind will fail. Given this point, use <codeBase> with caution.

The <codeBase> element can also be helpful when referencing assemblies located on a remote networked machine. Assume you have permission to access a folder located at http://www. IntertechTraining.com. To download the remote *.dll to the GAC’s download cache on your location machine, you could update the <codeBase> element as so:

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

387

<codeBase version="2.0.0.0" href="http://www.IntertechTraining.com/Assemblies/CarLibrary.dll" />

Source Code The CodeBaseClient application can be found under the Chapter 11 subdirectory.

The System.Configuration Namespace

Currently, all of the *.config files shown in this chapter have made use of well-known XML elements that are read by the CLR to resolve the location of external assemblies. In addition to these recognized elements, it is perfectly permissible for a client configuration file to contain application-specific data that has nothing to do with binding heuristics. Given this, it should come as no surprise that the .NET Framework provides a namespace that allows you to programmatically read the data within a client configuration file.

The System.Configuration namespace provides a small set of types you may use to read custom data from a client’s *.config file. These custom settings must be contained within the scope of an <appSettings> element. The <appSettings> element contains any number of <add> elements that define a key/value pair to be obtained programmatically.

For example, assume you have a *.config file for a console application named AppConfigReaderApp that defines a database connection string and a point of data named timesToSayHello:

<configuration>

<appSettings>

<add key="appConStr" value="server=localhost;uid='sa';pwd='';database=Cars" />

<add key="timesToSayHello" value="8" />

</appSettings>

</configuration>

Reading these values for use by the client application is as simple as calling the instance-level

GetValue() method of the System.Configuration.AppSettingsReader type. As shown in the following code, the first parameter to GetValue() is the name of the key in the *.config file, whereas the second parameter is the underlying type of the key (obtained via the C# typeof operator):

class Program

{

static void Main(string[] args)

{

//Create a reader and get the connection string value.

AppSettingsReader ar = new AppSettingsReader(); Console.WriteLine(ar.GetValue("appConStr", typeof(string)));

//Now get the number of times to say hello, and then do it!

int numbOfTimes = (int)ar.GetValue("timesToSayHello", typeof(int)); for(int i = 0; i < numbOfTimes; i++)

Console.WriteLine("Yo!");

Console.ReadLine();

}

}

The AppSettingsReader class type does not provide a way to write application-specific data to a *.config file. While this may seem like a limitation at first encounter, it actually makes good sense. The whole idea of a *.config file is that it contains read-only data that is consulted by the CLR (or possibly the AppSettingsReader type) after an application has already been deployed to a target machine.

388C 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

Note During our examination of ADO.NET (Chapter 22) you will learn about the new <connectionStrings> configuration element and new types within the System.Configuration namespace. These .NET 2.0–specific items provide a standard manner to handle connection string data.

Source Code The AppConfigReaderApp application can be found under the Chapter 11 subdirectory.

The Machine Configuration File

The configuration files you’ve examined in this chapter have a common theme: they apply only to

a specific application (that is why they have the same name as the launching application). In addition, each .NET-aware machine has a file named machine.config that contains a vast number of configuration details (many of which have nothing to do with resolving external assemblies) that control how the .NET platform operates.

The .NET platform maintains a separate *.config file for each version of the framework installed on the local machine. The machine.config file for .NET 2.0 can be found under the C:\WINDOWS\Microsoft.NET\Framework\v2.0.50215\CONFIG directory (your version may differ). If you were to open this file, you would find numerous XML elements that control ASP.NET settings, various security details, debugging support, and so forth. However, if you wish to update the machine.config file with machinewide application settings (via an <appSettings> element), you are free to do so.

Although this file can be directly edited using Notepad, be warned that if you alter this file incorrectly, you may cripple the ability of the runtime to function correctly. This scenario can be far more painful than a malformed application *.config file, given that XML errors in an application configuration file affect only a single application, but erroneous XML in the machine.config file can break a specific version of the .NET platform.

The Assembly Binding “Big Picture”

Now that you have drilled down into the details regarding how the CLR resolves the location of requested external assemblies, remember that the simple case is, indeed, simple. Many (if not most) of your .NET applications will consist of nothing more than a group of private assemblies deployed to a single directory. In this case, simply copy the folder to a location of your choosing and run the client executable.

As you have seen, however, the CLR will check for client configuration files and publisher policy assemblies during the resolution process. To summarize the path taken by the CLR to resolve an external assembly reference, ponder Figure 11-29.

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

389

 

 

 

 

 

Figure 11-29. Behold the CLR’s path of assembly resolution.

Summary

This chapter drilled down into the details of how the CLR resolves the location of externally referenced assemblies. You began by examining the content within an assembly: headers, metadata, manifests, and CIL. Then you constructed single-file and multifile assemblies and a handful of client applications (written in a language-agonistic manner).

As you have seen, assemblies may be private or shared. Private assemblies are copied to the client’s subdirectory, whereas shared assemblies are deployed to the Global Assembly Cache (GAC), provided they have been assigned a strong name. Finally, has you have seen, private and shared assemblies can be configured using a client-side XML configuration file or, alternatively, via a publisher policy assembly.

C H A P T E R 1 2

■ ■ ■

Type Reflection, Late Binding, and

Attribute-Based Programming

As shown in the previous chapter, assemblies are the basic unit of deployment in the .NET universe. Using the integrated object browsers of Visual Studio 2005, you are able to examine the public types within a project’s referenced set of assemblies. Furthermore, external tools such as ildasm.exe allow you to peek into the underlying CIL code, type metadata, and assembly manifest for a given .NET binary. In addition to this design-time investigation of .NET assemblies, you are also able to programmatically obtain this same information using the System.Reflection namespace. To this end, the first task of this chapter is to define the role of reflection and the necessity of .NET metadata.

The remainder of the chapter examines a number of closely related topics, all of which hinge upon reflection services. For example, you’ll learn how a .NET client may employ dynamic loading and late binding to activate types it has no compile-time knowledge of. You’ll also learn how to insert custom metadata into your .NET assemblies through the use of system-supplied and custom attributes. To put all of these (seemingly esoteric) topics into perspective, the chapter closes by demonstrating how to build several “snap-in objects” that you can plug into an extendable Windows Forms application.

The Necessity of Type Metadata

The ability to fully describe types (classes, interfaces, structures, enumerations, and delegates) using metadata is a key element of the .NET platform. Numerous .NET technologies, such as object serialization, .NET remoting, and XML web services, require the ability to discover the format of types at runtime. Furthermore, cross-language interoperability, compiler support, and an IDE’s IntelliSense capabilities all rely on a concrete description of type.

Regardless of (or perhaps due to) its importance, metadata is not a new idea supplied by the

.NET Framework. Java, CORBA, and COM all have similar concepts. For example, COM type libraries (which are little more than compiled IDL code) are used to describe the types contained within a COM server. Like COM, .NET code libraries also support type metadata. Of course, .NET metadata has no syntactic similarities to COM IDL. Recall that the ildasm.exe utility allows you to view an assembly’s type metadata using the Ctrl+M keyboard option (see Chapter 1). Thus, if you were to open any of the *.dll or *.exe assemblies created over the course of this book (such as CarLibrary.dll) using ildasm.exe and press Ctrl+M, you would find the relevant type metadata (see Figure 12-1).

391

392 C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

Figure 12-1. Viewing an assembly’s metadata

As you can see, ildasm.exe’s display of .NET type metadata is very verbose (the actual binary format is much more compact). In fact, if I were to list the entire metadata description representing the CarLibrary.dll assembly, it would span several pages. Given that this act would be a woeful waste of your time (and paper), let’s just glimpse into some key types of the CarLibrary.dll assembly.

Viewing (Partial) Metadata for the EngineState Enumeration

Each type defined within the current assembly is documented using a “TypeDef #n” token (where TypeDef is short for type definition). If the type being described uses a type defined within a separate

.NET assembly, the referenced type is documented using a “TypeRef #n” token (where TypeRef is short for type reference). A TypeRef token is a pointer (if you will) to the referenced type’s full metadata definition. In a nutshell, .NET metadata is a set of tables that clearly mark all type definitions (TypeDefs) and referenced entities (TypeRefs), all of which can be viewed using ildasm.exe’s metadata window.

As far as CarLibrary.dll goes, one TypeDef we encounter is the metadata description of the CarLibrary.EngineState enumeration (your number may differ; TypeDef numbering is based on the order in which the C# compiler processes the file):

TypeDef #1

-------------------------------------------------------

TypDefName:

CarLibrary.EngineState (02000002)

Flags

:

[Public]

[AutoLayout] [Class] [Sealed] [AnsiClass] (00000101)

Extends

:

01000001

[TypeRef] System.Enum

...

Field #2

-------------------------------------------------------

Field Name: engineAlive (04000002)

Flags

:

[Public] [Static] [Literal] [HasDefault] (00008056)

DefltValue:

(I4) 0

CallCnvntn:

[FIELD]

Field

type:

ValueClass CarLibrary.EngineState

...

Here, the TypDefName token is used to establish the name of the given type. The Extends metadata token is used to document the base class of a given .NET type (in this case, the referenced type, System.Enum). Each field of an enumeration is marked using the “Field #n” token. For brevity, I have simply listed the metadata for EngineState.engineAlive.

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

393

Viewing (Partial) Metadata for the Car Type

Here is a partial dump of the Car type that illustrates the following:

How fields are defined in terms of .NET metadata

How methods are documented via .NET metadata

How a single type property is mapped to two discrete member functions

TypeDef #3

-------------------------------------------------------

TypDefName:

CarLibrary.Car (02000004)

Flags

:

[Public]

[AutoLayout] [Class] [Abstract] [AnsiClass] (00100081)

Extends

:

01000002

[TypeRef] System.Object

Field #1

-------------------------------------------------------

Field Name:

petName (04000008)

Flags

:

[Family] (00000004)

CallCnvntn:

[FIELD]

Field type:

String

...

 

 

Method #1

 

 

-------------------------------------------------------

MethodName: .ctor (06000001)

Flags

: [Public] [HideBySig] [ReuseSlot] [SpecialName]

[RTSpecialName] [.ctor] (00001886)

RVA

: 0x00002050

ImplFlags : [IL] [Managed] (00000000) CallCnvntn: [DEFAULT]

hasThis ReturnType: Void No arguments.

...

Property #1

-------------------------------------------------------

Prop.Name :

PetName (17000001)

Flags

:

[none] (00000000)

CallCnvntn:

[PROPERTY]

hasThis ReturnType: String No arguments. DefltValue:

Setter

:

(06000004)

set_PetName

Getter

:

(06000003)

get_PetName

0 Others

 

 

 

...

First, note that the Car class metadata marks the type’s base class and includes various flags that describe how this type was constructed (e.g., [public], [abstract], and whatnot). Methods (such as our Car’s constructor) are described in regard to their parameters, return value, and name. Finally, note how properties are mapped to their internal get/set methods using the .NET metadata Setter/Getter tokens. As you would expect, the derived Car types (SportsCar and MiniVan) are described in a similar manner.