Customizing the Microsoft .NET Framework Common Language Runtime

At the time of this writing, three major versions of the .NET Framework have been released: versions 1.0, 1.1, and 2.0. If you're writing an extensible application that dynamically loads add-ins, it's likely you'll encounter an add-in built with a different version of the .NET Framework than your application is. If the add-in was built with an older version of the .NET Framework than your application was, it's likely that everything will work fine because of the CLR's commitment to backward compatibility. However, we all know that backward compatibility cannot be completely guaranteed. As a result, it's useful to know how the CLR behaves in a process containing assemblies built with multiple versions of the .NET Framework. As with the other topics discussed in this chapter, dynamic, extensible applications are likely to encounter a greater range of versioning scenarios than applications in which all dependencies are known when the application is compiled. There are five main points to keep in mind when considering how add-ins built with various versions of the CLR will affect your extensible application:

  • In general, it's not a good idea to try to load an add-in built with a newer version of the CLR than the version used to build your application. In fact, the CLR prevents you from loading an assembly built with .NET Framework 2.0 into a process that is running either .NET Framework 1.0 or .NET Framework 1.1. You can, however, load an assembly built with .NET Framework 1.1 into a process running .NET Framework 1.0, although it is not recommended.

  • Loading an add-in built with a version of the CLR older than the version used to build your application is generally OK. As described, the CLR's commitment to backward compatibility means the add-in has a pretty good chance of working. If a particular addin doesn't work in this scenario, it is sometimes possible to fix the problem by including version policy statements in your application's configuration file. I describe this in more detail later in the chapter when I discuss overriding .NET Framework unification in the section "Overriding Unification."

  • As the author of the extensible application, you get to pick which version of the CLR is loaded into your process. The add-ins do not have a say in which version of the CLR is selected.

  • Once you've selected a version of the CLR to load into the process, the CLR automatically enforces that a matching set of .NET Framework assemblies comes with it. This concept, called .NET Framework unification, ensures that a consistent set of .NET Framework assemblies is loaded into the process. I talk about .NET Framework unification later in this chapter in the section "Microsoft .NET Framework Unification."

  • If .NET Framework unification introduces an assembly into your process that causes something to stop working, you can use your application configuration file to override the CLR's choice of assembly version.

The rest of this section expands on these five points. Before I go on, however, look at a few .NET Framework APIs that are useful when dealing with versioning in extensible applications.

Determining Which Version of the CLR Was Used to Build an Assembly

As described in Chapter 4, every assembly contains information about the version of the .NET Framework it was compiled with. Being able to determine which version of the .NET Framework was used to build a particular add-in can be useful, especially if you begin to see problems in your application that you believe are version related. The version of the .NET Framework used to build an assembly can be obtained using the ImageRuntimeVersion property on System.Reflection.Assembly. Keep in mind, however, that the version number this property returns is the CLR version number, not the version number of the .NET Framework itself. For example, Assembly.ImageRuntimeVersion returns the string "v1.1.4322" for an assembly built with .NET Framework 1.1. Table 7-3 shows the CLR versions and the corresponding versions of the .NET Framework. Refer to Chapter 4 for a more complete description of how these version numbers relate.

Table 7-3. How CLR Version Numbers Map to .NET Framework Versions

CLR Version

.NET Framework Version

v1.0.3705

.NET Framework 1.0

v1.1.4322

.NET Framework 1.1

v2.0.41013

.NET Framework 2.0

There is one caveat when using the ImageRuntimeVersion property. The fact that ImageRuntimeVersion is a member of the Assembly class means that the CLR must load the assembly for which you'd like version information into the process before you can access the property. If you then decide that you don't want to use the assembly, you can't unload it without unloading the application domain containing the assembly. If you need to be able to determine which version of the CLR was used to build an assembly without having to load it, you need to use an unmanaged API. The CLR startup shim, mscoree.dll, provides an unmanaged API for exactly this purpose. This API, called GetFileVersion, takes an assembly's filename and returns the version number used to build that assembly in a buffer. Here's the signature for GetFileVersion from mscoree.idl in the .NET Framework SDK:

STDAPI GetFileVersion(LPCWSTR szFilename, LPWSTR szBuffer, DWORD cchBuffer, DWORD* dwLength)

The Extensible Application Chooses the Version

As discussed in Chapter 3, only one version of the CLR can be loaded into a given process. It's up to you, as the author of the extensible application, to decide which version to load. The add-ins that you dynamically load into your process have no say in which version of the CLR is loaded. Furthermore, no infrastructure currently available allows an add-in to express a dependency on a particular version of the CLR.

As discussed in Chapters 3 and 4, it's typically best to load the same version of the CLR that you used to build your application. Refer to those chapters for both the strategies to consider and the mechanics involved in loading the CLR.

At any point you can determine which version of the CLR is loaded into your process using the Version property on the System.Environment class. This property returns the CLR version, not the .NET Framework version. For example, the value of System.Environment.Version for a process running .NET Framework 2.0 is "2.0.41013." Table 7-3 contains the mapping between CLR version numbers and the corresponding versions of the .NET Framework.

Microsoft .NET Framework Unification

When you load an add-in assembly into your process, that assembly contains static references to the versions of the .NET Framework assemblies it was built against. For example, an assembly built with .NET Framework 1.1 has references to the 1.1 versions of System, System.XML, System.Data, and so on. If you load several add-ins into your process, some of which are built with different versions, you'll have references to multiple versions of the .NET Framework assemblies. Figure 7-10 shows a scenario in which an extensible application built against .NET Framework 2.0 has loaded add-ins built against all three versions of the .NET Framework.

Figure 7-10. An extensible application with references to add-ins built with multiple versions of the .NET Framework

Given this scenario, the question arises as to whether it is preferable to load multiple versions of the .NET Framework assemblies into the same process or redirect the various references to a single version of the .NET Framework assemblies. Clearly, there are arguments for doing it either way. On the one hand, it might be desirable to load the exact version of the .NET Framework assemblies that a given add-in has requested. After all, presumably the add-in was tested against this version; therefore, it has the best chance to work. On the other hand, loading multiple versions of the .NET Framework assemblies into the same process has two complications: one is technical, whereas the other is a matter of logistics. Although it is technically feasible to load multiple versions of the same assembly into a given application domain or process, complications arise if two add-ins built against different versions of the .NET Framework need to communicate by exchanging types. Because the identity of the assembly in which a type is contained is part of that type's identity, a given type from two different versions of the same assembly is considered a different type by the CLR. For example, an XMLDocument type from version 2.0.3600 of System.XML is a different type than the XMLDocument type from version 1.0.3705 of System.XML. As a result, the CLR throws an exception if an assembly tries to pass an instance of the 1.0.3705 version of XMLDocument to a method on a type expecting a 2.0.3600 version of XMLDocument. The amount different add-ins need to communicate with each other clearly varies by scenario, so it might be possible that this particular restriction isn't an issue for you. However, the other complication that arises when multiple versions of the .NET Framework assemblies are loaded simultaneously is related to the consistency between assemblies. From the beginning, the .NET Framework assemblies were built to work as a matched set. Several interdependencies between these assemblies must remain consistent. Also, because mixing and matching these assemblies hasn't been a priority yet, the amount of testing done by Microsoft to support these scenarios has been limited.

As a result of the complexities involved in loading multiple versions of the .NET Framework assemblies into a process or application domain, the default behavior of the CLR is to redirect all references to .NET Framework assemblies to the version of those assemblies that matches the CLR that is loaded into the process. The process of redirecting all references to this matched set is termed .NET Framework unification. The result of this unification is shown in Figure 7-11.

Figure 7-11. The CLR unifies all references to .NET Framework assemblies.

It's important to remember that only references to the .NET Framework assemblies are unified. All other assembly references are resolved as is (subject to version policy, of course). For example, say you load two add-in assemblies that reference different versions of the same shared assembly called AcmeGridControl. Because AcmeGridControl is not a .NET Framework assembly, references to it will not be unified. Instead, both versions are loaded. The following is a list of those assemblies that are unified by the CLR:

  • mscorlib

  • System

  • System.Xml

  • System.Data

  • System.Data.OracleClient

  • System.Runtime.Remoting

  • System.Windows.Forms

  • System.Web

  • System.Drawing

  • System.Design

  • System.Runtime.Serialization.Formatters.Soap

  • System.Drawing.Design

  • System.EnterpriseServices

  • System.DirectoryServices

  • System.Management

  • System.Messaging

  • System.Security

  • System.ServiceProcess

  • System.Web.Mobile

  • System.Web.RegularExpressions

  • System.Web.Services

  • System.Configuration.Install

  • Accessibility

  • CustomMarshalers

  • cscompmgd

  • IEExecRemote

  • IEHost

  • IIEHost

  • ISymWrapper

  • Microsoft.JScript

  • Microsoft.VisualBasic

  • Microsoft.VisualBasic.Vsa

  • Microsoft.VisualC

  • Microsoft.Vsa

  • Microsoft.Vsa.Vb.CodeDOMProcessor

  • Microsoft_VsaVb

  • mscorcfg

  • vjswfchtml

  • vjswfccw

  • VJSWfcBrowserStubLib

  • vjswfc

  • vjslibcw

  • vjslib

  • vjscor

  • VJSharpCodeProvider

Overriding Unification

If the unification of .NET Framework assembly references causes problems in your scenario, you can override the unification using version policy statements. You shouldn't have to resort to this too often because the CLR's backward compatibility has been pretty good so far. However, it's not inconceivable for you to encounter a compatibility issue that will cause you to want to load a different version of a .NET Framework assembly than the one the CLR selects by default. The best way to override unification is by issuing version policy statements in the configuration file for a particular application domain. This approach is preferable to using machine-wide policy because it affects only your application domain(s), not every process on the machine (also, it's often the case that the machine configuration file is secured, so you can't write to it anyway unless you're an administrator).

To see how this works, consider an example in which a particular add-in that you must load has a strict dependency on the version of System.XML that shipped with version 1.1 of the .NET Framework, but you are running .NET Framework 2.0 in your process. To redirect the reference to System.XML from .NET Framework 2.0 back down to .NET Framework 1.1, you'd author a configuration file that looks like this:

<?xml version="1.0"?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Xml" publicKeyToken="b77a5c561934e089" /> <bindingRedirect oldVersion="0.0.0.0-2.0.3600" newVersion="1.1.5000" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>

This configuration file causes version 1.1.5000 of System.XML (the version that shipped in .NET Framework 1.1) to be loaded regardless of which version is referenced. Given this configuration file, you have the choice of how widely you'd like to apply this redirection. It can be that you want 1.1.5000 to be the only version of System.XML that is loaded in your process. In this case, you'd assign your configuration file to every application domain you create using the ConfigurationFile property of AppDomainSetup as described in Chapter 6. You might also choose to load System.XML version 1.1.5000 only into the application domain in which the add-in that requires it is running. If so, add-ins in other application domains that reference System.XML will get the unified version (the version that ships with .NET Framework 2.0).

Note that it is also possible to use version policy statements to cause all references to the .NET Framework assemblies to be redirected for a particular application domain. If you start by redirecting one reference, you might find inconsistencies that cause you to want to redirect the entire set of references to .NET Framework assemblies. In this way, you can cause two parallel stacks of .NET Framework assemblies to be loaded into the same process, yet be isolated from each other using application domain boundaries as shown in Figure 7-12.

Figure 7-12. Using a configuration file to override .NET Framework unification

Note

Even though mscorlib is in the set of unified assemblies, you cannot use a bindingRedirect statement (or any other mechanism) to override the unification of mscorlib. The version of mscorlib to load is chosen by the CLR when the process starts and cannot be changed.

    Категории