Side-By-Side and Versioning Considerations for .NET Remoting

 

Piet Obermeyer and Jonathan Hawkins
Microsoft Corporation

August 2001
Updated March 2002

Summary: Developers building distributed applications must know how to ensure that applications can run side-by-side using different versions of the Common Language Runtime (CLR) and how to deal with strong-named assemblies when deploying distributed applications using remoting. This article provides a brief overview of the implications that versioning has on a distributed application by examining activation, method calling, and serialization in this context. (8 printed pages)

Contents

Introduction
Versioning Concepts
Building Strong-Named Assemblies
Running Side-By-Side
Using Strong-Named Assemblies
Well-Known Objects
Client-Activated Objects

Introduction

Maintaining large applications across versions has the habit of changing into a complex problem very quickly. This is especially true when DLLs undergo significant changes between versions and different applications on the same machine depend on the same DLL. Changes to the DLL to fix a problem in one application might break other applications relying on the same DLL. Microsoft .NET solves these problems by versioning assemblies, thereby allowing applications to run only with the versions they were built and tested with, unless this default policy is explicitly overridden in the application configuration file. When different versions of the same application are run side-by-side, the binding policy ensures that each application loads the right versions of the assemblies the application was built with, or in cases where policy files are used, the latest compatible version specified in the policy file.

In general, distributed applications have a more complex set of versioning issues than their local counterparts since clients and services are normally running on different machines. The following sections describe how .NET remoting deals with versioning policy across application domains and cross process.

Versioning Concepts

Each assembly has a version number as part of its identity. The version number is stored in the assembly manifest along with other identity information like relationships and identities of other assemblies connected with the application.

  • The version number is physically represented as a four part number containing <major version>.<minor version>.<build number>.<revision>. For example, version 1.5.1254.0 indicates 1 as the major version number, 5 as the minor version number, 1254 as the build number and 0 as the revision number.
  • Two assemblies that differ only by version number are considered by the runtime to be completely different assemblies.
  • The run time distinguishes between regular and strongly named assemblies for the purpose of versioning. Version checking only occurs with strongly named assemblies. If no key information is provided when an assembly is built, the development tools generate a regular assembly with version number 0.0.0.0.
  • When an assembly is built, the development tool records dependency information for each assembly that is referenced in the assembly manifest. The run time uses these version numbers in conjunction with configuration information set by the administrator, an application, or a publisher, to load the proper version of a referenced assembly.
  • When an application uses strong-named assemblies, it will always use the version number the application was built with unless this is explicitly overridden. This allows side-by-side execution of different versions of the same applications, thereby preventing the traditional problems encountered when a DLL is replaced with a different version.

The run time uses the following steps to resolve an assembly reference:

  1. Determines the correct assembly version by examining applicable configuration files, including the application, publisher policy, and machine policy configuration files. In a Microsoft Internet Explorer Web scenario where the configuration file is located on a remote machine, the run time must locate and download the application configuration file first. This step only occurs for strong-named assemblies. Since simple named assemblies don't undergo version checking, policy files are not checked.
  2. Checks whether the assembly name has been bound-to before and, if so, use the previously loaded assembly.
  3. Checks the global assembly cache. If the assembly is found there, the run time uses this assembly. This step only occurs for strong-named assemblies.
  4. Probes for the assembly using the following steps:
    1. If configuration and publisher policy do not affect the original reference and if the bind request was created using the Assembly.LoadFrom method, the run time checks for location hints.
    2. If a code base is found in the configuration files, the run time checks only this location. If this probe fails, the run time determines that the binding request failed and no other probing occurs.
    3. Probes for the assembly using the heuristics described in the probing section below. If the assembly is not found after probing, the run time requests the Windows Installer to provide the assembly. This acts as an install-on-demand feature.
    4. Finally the directory the caller loaded from is used as the last probing location.

Building Strong-Named Assemblies

Strong-named assemblies can easily be built by following the steps outlined below.

Obtain a key for the assembly by running the Strong Name Utility as follows:

sn /k <outfile>

A key file will be generated and saved to disk. Add this key to the assembly by adding the following to one of the source files used to build the assembly. A common practice is to add this information to a separate source file and compile this file with the rest of the source.

using System;
using System.Reflection;

[assembly:AssemblyKeyFile("Hello.key")]
[assembly:AssemblyVersion("2.0.0.0")]

Once the assembly has been compiled, it can be published using the Global Assembly Cache (GAC) Utility as follows

gacutil /i MyAssembly.dll

This step will copy the assembly to the global assembly cache. Strong-named assemblies can also be used without copying them to the GAC.

Running Side-By-Side

Side-by-side applies in the following two common scenarios:

  • Two different versions of the Common Language Runtime (CLR) might be installed on the same machine. In the absence of a policy file, applications should continue to work with the CLR version they were built with. If a policy file is present specifying that two different versions of an assembly like System.Run time.Remoting are compatible, Fusion will automatically load the latest compatible version. It is therefore important to ensure that only strong assembly names are used in configuration files when referring to CLR assemblies. For example, machine.config file specifies the HTTP remoting channel as follows:
    <channel id="http"
        type="System.Runtime.Remoting.Channels.Http.HttpChannel,
        System.Runtime.Remoting, 
        Version=1.0.3102.0, 
        Culture=neutral, 
        PublicKeyToken=b77a5c561934e089" />
    

    This entry ensures that this specific assembly will be loaded when the remoting application configures remoting.

  • Application developers should decide if their applications need to run side-by-side, and if so, should ensure that only strong names are used when calling Assembly.Load or when specifying user sink providers or custom channels in remoting configuration files.

Using Strong-Named Assemblies

Remoting was designed to work with strong name assemblies and although it does not introduce any new concepts, there are a few key points developers have to be aware of when building distributed applications. When strong names are used with remoting, the following basic rules apply:

  • Versions are always included with the TypeName property of a MethodCallMessage. When a client calls a method on a proxy, the version number of the proxy will be included in the method call. The server ignores this version number when you are calling a well-known object, and will create the latest version of the object to service the client. However, remoting sends an activation request for client-activated objects, the server will create the highest compatible version or the exact one if no policy file is present. If the class holder cannot resolve the assembly, remoting falls back on partial binding by attempting to load the assembly without the versioning information.
  • SOAPSUDS will not generate any version information when building proxies. You can use the –gc option to just generate the code; then compile the proxy yourself and specify the versioning information in an AssemblyInfo.cs, which contains the AssemblyKeyFile and AssemblyVersion assembly-level custom attributes.
  • Versions are always included with the ActivationTypeName of a ConstructionCallMessage.
  • Versions are always included with the TypeInfo stored in ObjRef.
  • Remoting uses serialization when objects are passed by value. When a client calls a method on the server that returns an object by value, there is a possibility that the assembly on the server was upgraded while the client was still running a previous version. When the formatter deserializes the object on the client, it falls back on partial binding if it cannot load the type using a strong name. This default behavior can be changed with the includeVersions and strictBinding attributes described in the Marshal By Value section later in this document.

The sections below describe how these rules affect object references and the different activation models commonly used in remoting. The discussion below refers to the version of a type rather than the version of the assembly when the type is defined. This is used for simplicity only and does not imply that types have versions; the unit of versioning is an assembly.

Well-Known Objects

Well-Known (Server Activated) objects are activated the first time a client calls a method on the object. In the case of SingleCall objects, a new instance of the object will be created to service each method call while a single instance of the object is created for Singletons. This instance services all clients calling methods on the object.

Clients connecting to well-known objects do not have any control over the version that will be activated, this is determined by the server. If no version information is provided when the service is configured, the latest version of the assembly will be used when the object is activated. For example, if we create two assemblies, MyHello 1.0.0.0 and MyHello 2.0.0.0, the well-known object will be activated using the version 2 assembly. It is important to note that this version will be used, irrespective of the version referenced when the client was built. The service can be configured to use a specific version of an assembly. The example below shows how to specify a version using a configuration file. Note: The culture and public token was omitted to keep the code snipped as short as possible.

<configuration>
<system.run time.remoting>
  <application name="RemotingHello">

    <lifetime leaseTime="20ms" sponsorshipTimeOut="20ms"
       renewOnCallTime="20ms" />

    <service>
      <wellknown mode="SingleCall" 
        type="Hello.HelloService,MyHello,
          Version=1.0.0.0"
        objectUri="HelloService.soap" />
      <activated type="Hello.AddService, MyHello"/>
    </service>

    <channels>
      <channel port="8000"
       ref="tcp">
      </channel>
    </channels>
  </application>
</system.run time.remoting>
</configuration>

In this example we specify that version 1.0.0.0 of the MyHello assembly should be used, so any clients connecting to this service will call version 1 of the HelloService. When more than one version of the same object is specified at the endpoint, the last version specified will be used when the object is activated. Any significant changes between versions of the same object can potentially have an adverse effect on clients. Let's say we added an additional parameter to a method or modified the type of one or more parameters on a method between version 1 and 2. Any clients compiled against version 1 of the assembly will throw an exception when the method is called on version 2 of the object. It is therefore recommended that a new version of an object be hosted at a different endpoint when any significant changes have taken place between versions.

Client-Activated Objects

When a client activates an object, a network call is immediately sent to the server where the requested object is activated and an object reference to the object is returned to the client. Since the client is in control of the activation of the object, the client is also in control of the version of the object that will be activated. For example, Version 1 of HelloService will be activated on the server if the client was built against Version 1 of the object, while Version 2 of HelloService will be activated on the server if the client was built against Version 2.

It is important to note that you cannot specify the version number for activated types when configuring the service, and any versioning information provided for well-known types have no effect on client-activated objects, even if both types are in the same assembly. For example, assume we have a client-activated object and a well-known object in the same assembly. We build one client against Version 1 of the assembly and the other client against Version 2. If no version information is specified for the well-known object, client 1 will connect to version 2 of a well-known object and version 1 of an activated object. Client 2 will connect to version 2 objects for both well-known and activated types.

If we now configure the service to use version 1 of the assembly for the well-known object, both clients will connect to version one of the well-known object, while client 1 connects to version 1 of the activated type and client 2 connects to version 2 of the activated type.

The version activated for a client cannot be configured. The version the client was built against will always be used.

Object References

The same rules that apply to well-known and activated types apply to object references as well. For example, when a proxy for a client-activated type is passed as a parameter from one client to another, or from a client to the server, the version information embedded in the object reference will be passed with it. When the receiver attempts to call a method on the proxy-generated form for the object reference, the version embedded in the latter takes precedence over the version the client was built against. For example, if Client 2 was built against Version 2 of an assembly, but it receives a Version 1 object reference as a parameter, all method calls made on the resulting proxy will be against Version 1 of the object activated on the server. The cast will fail in this case unless a policy is in place saying the types are equivalent.

In the case of well-known objects, the server dictates the version that will be used and all clients that receive an object reference as a parameter will communicate with the version specified when the service was configured. In the absence of any versioning, the latest version will be activated on the server.

Marshal By Value

When a marshal by value object is passed between application domains, the formatter has to cope with versions during serialization and deserialization. By default, versions will automatically be included when an object is serialized. During deserialization, the loader will attempt to load the type using the string name, and if this fails, it strips out the versioning information and tries to load the type. There is some risk in this approach since the type that is loaded might not be compatible with the one that was serialized but you have to make a tradeoff between throwing an exception versus attempting to load a slightly incompatible type. Since the version number is for the assembly rather than the type, is very possible that the type in question never changed between versions, so throwing an exception in this case breaks an application that will actually work.

When you serialize an object that does not have a version number, the latest version of the type will be loaded on the client.

Remoting provides two configuration attributes that can be used to control how version numbers are used during serialization and deserialization. The attribute includeVersions can be used on the sender side (where serialization is done) to turn off versioning. When this attribute is set to false, no versioning information will be included when the object is serialized. The default is true. Why would you want to turn off versioning? Assume the sender (server) knows that the version number of an assembly has changed but objects being sent by value will not be affected. Since you don't know if the formatter on the receiving side has been configured for strict binding, removing versioning from the sending side ensures the receiver (client) will be able to deserialize the type. The snippet below shows how this attribute can be set in the remoting configuration file.

<formatter ref="soap" includeVersions="false" />

It is also possible to configure the formatter on the receiving side to enforce versioning by setting the strictBinding attribute to true as follows:

<formatter ref="soap" strictBinding="true" />

When this attribute is on, the formatter will not attempt to load the type using partial binding but will throw an exception instead. If no versioning is included with the type, the latest version of the object will be loaded.

Show: