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

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

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

512 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Configuring Shared Assemblies

Like a private assembly, shared assemblies can be configured using a client *.config file. Of course, because shared assemblies are deployed to a well-known location (the GAC), you will not use the <privatePath> element as you did for private assemblies (although if the client is using both shared and private assemblies, the <privatePath> element may still exist in the *.config file).

You can use application configuration files in conjunction with shared assemblies whenever you wish to instruct the CLR to bind to a different version of a specific assembly, effectively bypassing the value recorded in the client’s manifest. This can be useful for a number of reasons. For example, imagine that you have shipped version 1.0.0.0 of an assembly and discover a major bug some time after the fact. One corrective action would be to rebuild the client application to reference the correct version of the bug-free assembly (say, 1.1.0.0) and redistribute the updated client and new library to each and every target machine.

Another option is to ship the new code library and a *.config file that automatically instructs the runtime to bind to the new (bug-free) version. As long as the new version has been installed into the GAC, the original client runs without recompilation, redistribution, or fear of having to update your resume.

Here’s another example: you have shipped the first version of a bug-free assembly (1.0.0.0), and after a month or two, you add new functionality to the assembly in question to yield version 2.0.0.0. Obviously, existing client applications that were compiled against version 1.0.0.0 have no clue about these new types, given that their code base makes no reference to them.

New client applications, however, wish to make reference to the new functionality found in version 2.0.0.0. Under .NET, you are free to ship version 2.0.0.0 to the target machines, and have version 2.0.0.0 run alongside the older version 1.0.0.0. If necessary, existing clients can be dynamically redirected to load version 2.0.0.0 (to gain access to the implementation refinements), using an application configuration file without needing to recompile and redeploy the client application.

Freezing the Current Shared Assembly

To illustrate how to dynamically bind to a specific version of a shared assembly, open Windows Explorer and copy the current version of the compiled CarLibrary project (1.0.0.0) into a distinct subdirectory (I called mine “CarLibrary Version 1.0.0.0”) to symbolize the freezing of this version (see Figure 15-24).

Figure 15-24. Freezing the current version of CarLibrary.dll

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

513

Building Shared Assembly Version 2.0.0.0

Now, open your existing CarLibrary project and update your code base with a new enum named MusicMedia that defines four possible musical devices:

// Holds source of music. public enum MusicMedia

{

musicCd,

musicTape,

musicRadio,

musicMp3

}

As well, add a new public method to the Car type that allows the caller to turn on one of the given media players (be sure to import the System.Windows.Forms namespace within Car.cs if necessary):

public abstract class Car

{

...

public void TurnOnRadio(bool musicOn, MusicMedia mm)

{

if(musicOn)

MessageBox.Show(string.Format("Jamming {0}", mm)); else

MessageBox.Show("Quiet time...");

}

...

}

Update the constructors of the Car class to display a MessageBox that verifies you are indeed using CarLibrary 2.0.0.0:

public abstract class Car

{

...

public Car()

{

MessageBox.Show("CarLibrary Version 2.0!");

}

public Car(string name, short max, short curr)

{

MessageBox.Show("CarLibrary Version 2.0!"); petName = name; maxSpeed = max; currSpeed = curr;

}

...

}

Finally, before you recompile, be sure to update this version of this assembly to 2.0.0.0 by updating the value passed to the [AssemblyVersion] attribute:

// CarLibrary version 2.0.0.0 (now with music!)

[assembly: AssemblyVersion("2.0.0.0")]

If you look in your project’s \bin\Debug folder, you’ll see that you have a new version of this assembly (2.0.0.0), while version 1.0.0.0 is safe in storage under the CarLibrary Version 1.0.0.0 directory. Install this new assembly into the GAC as described earlier in this chapter. Notice that you now have two versions of the same assembly (see Figure 15-25).

514 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Figure 15-25. Side-by-side execution of a shared assembly

If you were to run the current SharedCarLibClient.exe program by double-clicking the icon using Windows Explorer, you should not see the “CarLibrary Version 2.0!” message box appear, as the manifest is specifically requesting version 1.0.0.0. How then can you instruct the CLR to bind to version 2.0.0.0? Glad you asked!

Note Visual Studio 2008 will automatically reset references when you compile your applications! Therefore, if you were to run your SharedCarLibClient.exe application within Visual Studio 2008, it will grab CarLibrary.dll version 2.0.0.0! If you accidentally ran your application in this way, simply delete the current CarLibrary.dll reference and select version 1.0.0.0 (which I suggested you place in a folder named CarLibrary Version 1.0.0.0).

Dynamically Redirecting to Specific Versions of a

Shared Assembly

When you wish to inform the CLR to load a version of a shared assembly other than the version listed in its manifest, you may build a *.config file that contains a <dependentAssembly> element. When doing so, you will need to create an <assemblyIdentity> subelement that specifies the friendly name of the assembly listed in the client manifest (CarLibrary, for this example) and an optional culture attribute (which can be assigned an empty string or omitted altogether if you wish to specify the default culture for the machine). Moreover, the <dependentAssembly> element will define a <bindingRedirect> subelement to define the version currently in the manifest (via the oldVersion attribute) and the version in the GAC to load instead (via the newVersion attribute).

Create a new configuration file in the application directory of SharedCarLibClient named SharedCarLibClient.exe.config that contains the following XML data. Of course, the value of your public key token will be different from what you see in the following markup, and it can be obtained either by examining the client manifest using ildasm.exe or via the GAC.

<configuration>

<runtime>

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

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

515

<assemblyIdentity name="CarLibrary" publicKeyToken="219ef380c9348a38" culture="neutral"/>

<bindingRedirect oldVersion= "1.0.0.0" newVersion= "2.0.0.0"/>

</dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

Now run the SharedCarLibClient.exe program by double-clicking the executable from the Windows Explorer. You should see the message that displays version 2.0.0.0 has loaded.

Multiple <dependentAssembly> elements can appear within a client’s configuration file. Although you have no need to do so for this example, assume that the manifest of SharedCarLibClient.exe also referenced version 2.5.0.0 of an assembly named MathLibrary. If you wished to redirect to version 3.0.0.0 of MathLibrary (in addition to version 2.0.0.0 of CarLibrary), the SharedCarLibClient.exe.config file would look like the following:

<configuration>

<runtime>

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

<!-- Controls Binding to CarLibrary -->

<dependentAssembly> <assemblyIdentity name="CarLibrary"

publicKeyToken="219ef380c9348a38"

culture=""/>

<bindingRedirect oldVersion= "1.0.0.0" newVersion= "2.0.0.0"/> </dependentAssembly>

<!-- Controls Binding to MathLibrary -->

<dependentAssembly>

<assemblyIdentity name="MathLibrary" publicKeyToken="219ef380c9348a38" culture=""/>

<bindingRedirect oldVersion= "2.5.0.0" newVersion= "3.0.0.0"/> </dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

Note It is possible to specify a range of old version numbers via the oldVersion attribute; for example,

<bindingRedirect oldVersion="1.0.0.0-1.2.0.0" newVersion="2.0.0.0"/> informs the CLR to use version 2.0.0.0 for any older version within the range of 1.0.0.0 to 1.2.0.0.

Revisiting the .NET Framework Configuration Utility

As you would hope, you can generate configuration details for shared assemblies using the graphical .NET Framework Configuration utility. Like the process of building a *.config file for private assemblies, the first step is to reference the *.exe to configure. To illustrate, delete the

SharedCarLibClient.exe.config you just authored. Now, add a reference to SharedCarLibClient.exe by right-clicking the Applications node. Once you do, expand the plus sign (+) icon and select the

516CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Configured Assemblies subnode. From here, click the Configure an Assembly link on the right side of the utility.

At this point, you are presented with a dialog box that allows you to establish a <dependentAssembly> element using a number of friendly UI elements. First, select the “Choose an assembly from the list of assemblies this application uses” radio button and click the Choose Assembly button.

A dialog box now displays that shows you not only the assemblies specifically listed in the client manifest, but also the assemblies referenced by these assemblies. For this example’s purposes, select CarLibrary. When you click the Finish button, you will be shown a Properties page for this one small aspect of the client’s manifest. Here, you can generate the <dependentAssembly> using the Binding Policy tab.

Once you select the Binding Policy tab, you can set the oldVersion attribute (1.0.0.0) via the Requested Version text field and the newVersion attribute (2.0.0.0) using the New Version text field. Once you have committed the settings, you will find the following configuration file is generated for you:

<?xml version="1.0"?> <configuration>

<runtime>

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

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

<publisherPolicy apply="yes" />

<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" /> </dependentAssembly>

</assemblyBinding>

</runtime>

</configuration>

Investigating the Internal Composition of the GAC

At this point, you have deployed and configured private and shared assemblies. Before we turn to the topic of publisher policy, let’s investigate the internal composition of the GAC itself. When you view the GAC using Windows Explorer, you find a number of icons representing each version of a shared assembly. This graphical shell is provided courtesy of a COM server named shfusion.dll. As you may suspect, however, beneath these icons is an elaborate (but predictable) directory structure.

To understand what the GAC really boils down to, open a command prompt and change to the Assembly directory:

cd c:\windows\assembly

Issue a dir command from the command line. Here you will find a folder named GAC_MISL (see Figure 15-26).

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

517

Figure 15-26. The hidden GAC_MSIL subdirectory

Change to the GAC_MSIL directory and issue a dir command once more. You will now be presented with a list of a number of subdirectories that happen to have the same exact name as the icons displayed by shfusion.dll. Change to the CarLibrary subdirectory and again issue a dir command (see Figure 15-27).

Figure 15-27. Inside the hidden CarLibrary subdirectory

As you can see, the GAC maintains a subdirectory for each version of a shared assembly, which follows the naming convention <versionOfAssembly>__PublicKeyToken. If you were again to change the current directory to version 1.0.0.0 of CarLibrary, you would indeed find a copy of the code library (see Figure 15-28).

518 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Figure 15-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 (which must be entered on a single line within the command window):

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

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

519

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. Using this technique, you are able to design a “machinewide” redirection for all applications using a specific version (or range of versions) of an existing assembly.

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 the GAC of 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 end points, or an arbitrary local directory outside a client’s application directory).

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. 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?). If you are interested, you can view the content of your machine’s download cache by supplying the /ldl option to gacutil.exe:

gacutil /ldl

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).

520 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

To see the <codeBase> element in action, create a Console Application named CodeBaseClient, set a reference to CarLibrary.dll version 2.0.0.0, and update the initial file as follows:

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=" CarLibrary" 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 locate the external assembly under C:\MyAsms.

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 follows:

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

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

521

Source Code The CodeBaseClient application can be found under the Chapter 15 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 applicationspecific 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 an App.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=

"Data Source=localhost;Initial Catalog=AutoLot;Integrated Security=True" /> <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):

using System.Configuration;

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Reading <appSettings> Data *****\n");

//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("Howdy!");

Console.ReadLine();

}

}

The AppSettingsReader class type does not provide a way to write application-specific data to a *.config file. However, if you ever needed to programmatically add new <appSettings> elements to