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

CSharp Bible (2002) [eng]-1

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

Figure 31-2: Viewing the GAC as a Windows folder

You can place assemblies with different strong names side by side in the global assembly cache, even if the segment names match. For example, version 1.0.0.0 of an assembly named assembly.dll can be installed in the global assembly cache along with version 2.0.0.0 of an assembly also named assembly.dll. Code that references a strongly named assembly has the assembly's strong name listed in the manifest and always binds to the assembly with that strong name, even if other assemblies with some components of the strong name are the same. If an entry assembly references version 1.0.0.0 of assembly.dll, for example, the .NET Framework always loads version 1.0.0.0 of assembly.dll into the process space of the entry assembly, even if other versions of assembly.dll exist in the GAC.

Setting the information that comprises a strong name for an assembly is as easy as adding some attributes to one of the source code files for a project. These attributes can be added to a source file containing C# code or can be added to a separate file containing only the attributes themselves.

Note Visual Studio .NET adds a source file called AssemblyInfo.cs to new C# projects and places attributes needed for strongly named assemblies into that source file. The filename can be changed once it is created and will still work, as long as the renamed file remains a part of the project build.

Setting version information

You can set assembly version information using an attribute called AssemblyVersion. The AssemblyVersion attribute takes a string describing the version number for the assembly, which is a series of four integers. The first integer is the major version number; the second integer is the minor version number; the third integer is the build number; and the fourth integer is the revision number. You can specify all four numbers for an assembly using the AssemblyVersion attribute:

[assembly: AssemblyVersion("1.0.0.0")]

As a shortcut, the C# compiler generates a revision number automatically if an asterisk is used instead of a revision number:

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

This syntax instructs the C# compiler to assign a revision number of its choosing to the assembly. The C# compiler calculates the number of seconds between midnight and the time you are compiling your code, divides the number by two, and uses the remainder from that division that number as the basis for generating a unique revision number. (This is called a modulo 2 operation, as a modulo operation computes the remainder of the division between two operands.) This enables a unique version number to be generated for every build.

As a further shortcut, the C# compiler generates a build number and a revision number automatically if an asterisk is used as the build number:

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

This syntax instructs the C# compiler to assign a build number and a revision number of its choosing to the assembly. In addition to the automatic revision number calculation already described, the C# compiler also calculates a build number using the number of days between January 1, 2000 and the day the code is compiled.

Setting culture information

Culture information specifies the culture for which the assembly is designed. Culture information is specified using an attribute called AssemblyCulture. The culture attribute accepts a string describing the culture for which the assembly is designed. The string can be specified with an empty string, which informs the .NET Framework that the assembly is culture-neutral and does not contain any culture-specific code or resources:

[assembly: AssemblyCulture("")]

The string can also specify a language and country for which the assembly was designed. The string that specifies the language and country information must be in a format documented in Internet RFC 1766, which takes the form of language-country:

[assembly: AssemblyCulture("en-US")]

Tip The RFC 1766 standard is titled "Tags for the Identification of Languages," and is available on the Internet at http://www.ietf.org/rfc/rfc1766.txt.

Only DLL-based assemblies should specify culture information. EXE-based assemblies should use an empty string for culture information. Specifying culture information for an EXE-based assembly results in an error from the C# compiler.

Setting key information

The .NET Framework ships with a command-line utility called the strong name utility, or sn for short, which helps build strong names for .NET assemblies. One of its most frequently used features enables the generation of a new set of digital signature keys that can be installed into an assembly and used as a part of the strong name for an assembly. The keys are written into a binary file, whose name is specified with the -k argument to the sn utility, as shown in the following command line:

sn -k KeyPair.snk

This command line instructs the sn utility to generate a new pair of digital signature keys and output the keys to a binary file named KeyPair.snk. This file is named as an argument to an attribute called AssemblyKeyFile:

[assembly: AssemblyKeyFile("KeyPair.snk")]

The .snk extension is not required. Any extension can be used for the key file generated by the strong name utility.

Working with the Assembly class

Now that you have been introduced to the Assembly class, you can take a closer look at it. The following sections describe how you can use its properties and methods.

Finding assembly location information

The Assembly class contains properties that describe the location of an assembly. A Location property specifies the location of the file that contains the manifest for the assembly. A CodeBase property specifies the location of the assembly as a Uniform Resource Identifier (URI). The related EscapedCodeBase property specifies the URI for the assembly, with special characters replaced with the equivalent escape codes (spaces in CodeBase values, for instance, are replaced with the escape sequence %20 in the EscapedCodeBase property). The GlobalAssemblyCache property is a Boolean that evaluates to True if the assembly is loaded from the GAC, and False otherwise.

Listing 31-2 shows a simple console application that gets a reference to the application's entry assembly and sends its location information to the console.

Listing 31-2: Examining Location Information

using System;

using System.Reflection;

public class MainClass

{

static void Main()

{

Assembly EntryAssembly;

EntryAssembly = Assembly.GetEntryAssembly(); Console.WriteLine("Location: {0}", EntryAssembly.Location); Console.WriteLine("Code Base: {0}", EntryAssembly.CodeBase); Console.WriteLine("Escaped Code Base: {0}", EntryAssembly.

EscapedCodeBase);

Console.WriteLine("Loaded from GAC: {0}", EntryAssembly.GlobalAssemblyCache);

}

}

Compiling and executing the code in Listing 31-2 sends the following information to the console:

Location: C:\Documents and Settings\User\My Documents\Listing31- 2\bin\Debug\Listing31-2.exe

Code Base: file:///C:/Documents and Settings/User/My Documents/Listing31- 2/bin/Debug/Listing31-2.exe

Escaped Code Base: file:///C:/Documents%20and%20Settings/User/My%20Documents/Listing31- 2/bin/Debug/Listing31-2.exe

Loaded from GAC: False

The path information varies depending on the actual location of the code when it is executed, but the results are basically the same: The Location property references a location on disk, and the CodeBase and EscapedCodeBase properties reference the assembly location as URIs.

Finding assembly entry points

Some assemblies contain entry points. Think of entry points as the "starting method" for an assembly. The most obvious example of an entry point for an assembly is the Main() method found in C#-based executables. The CLR loads an executable, searches for the entry point for the assembly, and begins executing with that entry point method.

DLL-based assemblies, by contrast, do not typically have entry points. These assemblies generally hold resources or types that are used by other pieces of code, and they are passive in that they wait to be called before any of the code in the assembly is executed.

The Assembly class contains a property called EntryPoint. The EntryPoint property is a value of a type called MethodInfo, which is found in the .NET System.Reflection namespace. The MethodInfo class describes the specifics of a method, and calling ToString() on an object of type MethodInfo returns a string that describes the method's return type, name, and parameters. The EntryPoint property is null if the assembly reference does not have an entry point, or a valid MethodInfo object if the assembly reference does have an entry point, as shown in Listing 31-3.

Listing 31-3: Working with an Assembly Entry Point

using System;

using System.Reflection;

public class MainClass

{

static void Main(string[] args)

{

Assembly EntryAssembly;

EntryAssembly = Assembly.GetEntryAssembly(); if(EntryAssembly.EntryPoint == null)

Console.WriteLine("The assembly has no entry point."); else

Console.WriteLine(EntryAssembly.EntryPoint.ToString());

}

}

Compiling and executing Listing 31-3 sends the following information to the console:

Void Main(System.String[])

In the simple example of Listing 31-3, the EntryPoint property is never null, but it is always good practice to check for the possibility of a null value, especially with more complicated pieces of code.

Loading assemblies

In many applications, assemblies that contain types needed by an application are referenced when the application is built. It is also possible, however, to load assemblies programmatically. There are several ways to load an assembly dynamically, and each of these loading techniques returns an Assembly object that references the loaded assembly.

The first assembly loading technique uses a static assembly method called Load(). The Load() method takes a string naming the assembly to be loaded. If the named assembly cannot be found, the Load() method throws an exception. By contrast, the LoadWithPartialName() method searches both the application directory and the GAC for the specified assembly, using as much naming information as available from the caller. Listing 31-4 illustrates the difference between these two methods.

Listing 31-4: Loading Assemblies Dynamically with Load() and LoadWithPartialName()

using System;

using System.Reflection; using System.IO;

public class AssemblyLoader

{

private Assembly LoadedAssembly;

public AssemblyLoader(string LoadedAssemblyName, bool PartialName)

{

try

 

{

");

Console.WriteLine("+---------------------

Console.WriteLine("| Loading Assembly {0}",

LoadedAssemblyName);

");

Console.WriteLine("+---------------------

if(PartialName == true) LoadedAssembly = Assembly.LoadWithPartialName(LoadedAssemblyName);

else

LoadedAssembly = Assembly.Load(LoadedAssemblyName);

WritePropertiesToConsole();

}

catch(FileNotFoundException)

{

Console.WriteLine("EXCEPTION: Cannot load assembly.");

}

}

private void WritePropertiesToConsole()

{

Console.WriteLine("Full Name: {0}", LoadedAssembly.FullName);

Console.WriteLine("Location: {0}", LoadedAssembly.Location);

Console.WriteLine("Code Base: {0}", LoadedAssembly.CodeBase);

Console.WriteLine("Escaped Code Base: {0}", LoadedAssembly.EscapedCodeBase);

Console.WriteLine("Loaded from GAC: {0}", LoadedAssembly.GlobalAssemblyCache);

}

}

public class MainClass

{

static void Main(string[] args)

{

AssemblyLoader Loader;

Loader = new AssemblyLoader("System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", false);

Loader = new AssemblyLoader("System.Xml", false); Loader = new AssemblyLoader("System.Xml", true);

}

}

Listing 31-4 illustrates a class called AssemblyLoader, whose constructor accepts an assembly name and a Boolean flag specifying whether the named assembly should be loaded using a partial name. The constructor loads the assembly and then calls a private method to print some of the base-naming and location properties of the loaded assembly to the console.

The Main() method in Listing 31-4 creates new objects of the AssemblyLoader class and attempts to load the .NET Framework System.XML assembly, found in the GAC, in various ways.

Executing Listing 31-4 results in the following information being written out to the console:

+---------------------

| Loading Assembly System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyTok

en=b77a5c561934e089 +---------------------

Full Name: System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5

c561934e089

Location: c:\windows\assembly\gac\system.xml\1.0.3300.0__b77a5c561934e089 \system

.xml.dll Code Base:

file:///c:/windows/assembly/gac/system.xml/1.0.3300.0__b77a5c561934e0 89/system.xml.dll

Escaped Code Base:

file:///c:/windows/assembly/gac/system.xml/1.0.3300.0__b77a5c 561934e089/system.xml.dll

Loaded from GAC: True +---------------------

| Loading Assembly System.Xml +---------------------

EXCEPTION: Cannot load assembly. +---------------------

| Loading Assembly System.Xml +---------------------

Full Name: System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5

c561934e089

Location: c:\windows\assembly\gac\system.xml\1.0.3300.0__b77a5c561934e089 \system

.xml.dll Code Base:

file:///c:/windows/assembly/gac/system.xml/1.0.3300.0__b77a5c561934e0 89/system.xml.dll

Escaped Code Base: file:///c:/windows/assembly/gac/system.xml/1.0.3300.0__b77a5c 561934e089/system.xml.dll

Loaded from GAC: True

Look closely at what is happening here. In the first case, the Main() method specifies the strong name for the System.Xml assembly, including its name, public key, version information and culture specifics. Because the System.Xml assembly is in the GAC, it is not stored in the application's directory, and the Load() method is unable to find the assembly in the directory containing the executable for Listing 31-4. However, because the strong name for the assembly was specified, the Load() method has enough information to search for the assembly in the GAC. The Load() method can find the assembly in the GAC and the loading operation succeeds.

In the second case, the Main() method specifies only the base name of the System.Xml assembly. Because the System.Xml assembly is in the GAC, it is not stored in the application's directory, and the Load() method is unable to find the assembly in the directory containing the executable for Listing 31-4. In addition, the Load() method does not have enough information to locate the assembly in the GAC, as multiple instances of System.Xml might exist in the GAC with different version numbers or public keys, and the load fails.

In the third and final case, the Main() method specifies only the base name of the System.Xml assembly and instructs the loader to find an assembly using only a partial name. Again, because the System.Xml assembly is in the GAC, it is not stored in the application's directory and the LoadWithPartialName() method is unable to find the assembly in the directory containing the executable for Listing 31-4. However, the LoadWithPartialName() method takes the partially supplied name and attempts to match the name to an assembly in the GAC. Because a partial name of System.Xml is supplied, and because there is an assembly with the name System.Xml in the GAC, the load operation succeeds.

Caution Using the LoadWithPartialName() method is not recommended. If the partially named assembly has multiple copies in the GAC - perhaps with different version numbers, cultures, or public keys - the actual instance loaded may not be the expected version. In addition, the loaded instance may be a different version than the

one you were expecting to load once newer versions of the assembly are loaded into the GAC. Use Load() instead of LoadWithPartialName() wherever possible.

Working with assembly type information

Assemblies can contain types, resources, or a combination of both. After an assembly is loaded, information about the types found in the assembly can be obtained. In addition, instances of the types can be created programmatically. Listing 31-5 illustrates these concepts.

Listing 31-5: Finding and Creating Assembly Types

using System;

using System.Reflection;

public class MainClass

{

static void Main(string[] args)

{

Assembly XMLAssembly; Type[] XMLTypes;

XMLAssembly = Assembly.Load("System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

XMLTypes = XMLAssembly.GetExportedTypes(); foreach(Type XMLType in XMLTypes)

{

object NewObject;

try

{

Console.Write(XMLType.ToString());

NewObject = XMLAssembly.CreateInstance(XMLType.ToString()); if(NewObject != null)

Console.WriteLine(" - Creation successful"); else

Console.WriteLine(" - CREATION ERROR");

}

catch(Exception e)

{

Console.WriteLine(" - EXCEPTION: {0}", e.Message);

}

}

}

}

The code in Listing 31-5 loads the System.Xml assembly from the GAC and calls a method on the Assembly class called GetExportedTypes() to obtain an array of type objects representing types that are found in the assembly and can be used or exported outside of the assembly. The code visits each type in the returned array and calls another assembly method called CreateInstance() to create an object instance of the named type. If the creation is successful, the CreateInstance() method returns a valid object reference. If the creation is not successful, CreateInstance() either returns a null object reference or throws an exception, depending upon the nature of the error.

Following are the first several lines of output from Listing 31-5:

System.Xml.XPath.XPathNavigator - EXCEPTION: Constructor on type System.Xml.XPath.XPathNavigator not found. System.Xml.IHasXmlNode - EXCEPTION: Constructor on

type System.Xml.IHasXmlNode not found. System.Xml.XPath.XPathNodeIterator - EXCEPTION: Constructor on type System.Xml.XPath.XPathNodeIterator not found. System.Xml.EntityHandling - Creation successful System.Xml.IXmlLineInfo - EXCEPTION: Constructor on type System.Xml.IXmlLineInfo not found.

System.Xml.XmlNameTable - EXCEPTION: Constructor on type System.Xml.XmlNameTable not found.

System.Xml.NameTable - Creation successful System.Xml.ReadState - Creation successful System.Xml.ValidationType - Creation successful System.Xml.WhitespaceHandling - Creation successful

The exceptions result because not all of the exported types have constructors, and only reference types with suitable constructors can be created with CreateInstance().

Tip After a reference to a Type object is obtained, a reference to the assembly containing the type can be found in the Assembly property of the Type object. This lets code discover the assembly that references a type.

Generating Native Code for Assemblies

When the CLR needs to execute code in an assembly, it passes the code through a just-in-time (JIT) compiler and turns the MSIL to native code that can be executed by the machine's CPU. The advantage to this JIT design is that you can ship MSIL code without having to worry about optimizing your code for the target processor. The .NET Framework will most likely be ported to a variety of CPU architectures found in a variety of devices from computers to handheld systems, and attempting to write optimal code for each of those processors would be a daunting task. This work is unnecessary, because each implementation of the .NET Framework ships with a JIT compiler that turns MSIL instructions into instructions optimal for the target CPU.

If performance is of the utmost concern in your application, you can turn your MSIL code into CPU-specific machine code through a process known as native image generation. During this process, MSIL instructions found in an assembly are translated into native CPU-specific instructions, which can then be written to disk. After this native image generation has completed, the CLR can make use of that code and can skip the JIT step normally employed for assemblies.

The .NET Framework ships with a tool called the Native Image Generator, which generates a native image for an assembly. This command-line tool is found in an executable called ngen.exe and takes an assembly name as input:

ngen assembly

The native image is placed in a cache of native images for assemblies.

Keep in mind that ngen must be run on the device that executes the generated code. You cannot, for example, build assemblies as part of your build process, run ngen on the assemblies, and ship the native images to your customers. Your build machine might very well have a different CPU than your customer's machines, and ngen generates code for the CPU on which ngen is executing. If it is important for your customers to have native images for your assemblies, you must run ngen on the customer's machines as a part of the installation process.

It is also important to note that the original .NET assemblies must be available at all times, even if native code is available in the native image cache. The native images are standard Win32 Portable Executable (PE) files and lack all of the metadata available in a .NET assembly. If code loads your native image assembly and executes code that forces the .NET Framework to examine metadata (using Reflection, for example, to obtain type information for the assembly), then the original .NET assembly must be available so that the CLR can query its metadata. The metadata will not be carried with the native image.

Summary

In this chapter, you examined the concept of a .NET assembly from the perspective of C# applications that access assembly information. Access to assembly information is performed through the Assembly class. The Assembly class exposes naming information for the assembly and enables assemblies to be loaded dynamically. Types managed by the assembly can be created on the fly.

You can apply the concepts illustrated in this chapter to build some very powerful .NET applications. Some of the tools that ship with the .NET Framework, such as the ILDASM tool, use a combination of Assembly class methods and other classes in the System.Reflection namespace to provide a fully detailed view of already compiled assemblies. You can obtain a great deal of information from assemblies, even if the source code used to build the assembly is not available, using the methods in the Assembly class and other reflection classes.

Chapter 32: Reflection

In This Chapter

An important characteristic of the .NET Framework is its ability to discover type information at runtime. Specifically, you can use the reflection namespace to view type information contained within assemblies which you can later bind to objects and you can even use this namespace to generate code at runtime. This technology extends the COM Automation technology with which many of you may be familiar.

As a programmer, you might often need to use an object without quite understanding what the object does. Using Reflection, you can take an object and examine its properties, methods, events, fields, and constructors. Because Reflection revolves around System.Type, you can examine an assembly and use methods, such as GetMethods() and GetProperties(), to return member information from the assembly. With this information, you can dig in deeper using the MethodInfo() method to return parameter lists and even call methods within the assembly with a method called Invoke.

Соседние файлы в предмете Программирование на C++