Dan's Blog

Software Engineer - Red Gate Software

Strong Naming + Remoting = Noth'g but ye liveliest Awfulness

Published Thursday, August 30, 2007 5:51 PM

"What you sente, did not Worke, whether because of Any Thing miss'g, or because ye Wordes were not Righte from my Speak'g or yr Copy'g. I alone am at a Loss...Certainely, there was Noth'g but ye liveliest Awfulness in that which H. rais'd upp from What he cou'd gather onlie a part of."   --  H P Lovecraft

Strong naming assemblies has many advantages. My favourites:

  • Trust. Users of the assembly can trust that it came from the signee, such as Red Gate Software, thanks to the public key cryptography involved.
  • Tamper prevention. Users of the assembly can trust that nobody has tampered with it since the signee released it. The .NET runtime will not load assemblies whose signed hash doesn't match their current hash.
  • Compatibility with the GAC. The global application cache only accepts strong named assemblies.

It also has disadvantages. My favourites:

  • "The const problem", or "One naming policy to rule them all". Strongly named assemblies cannot use assemblies which aren't strongly named. Like a virus, strong naming must spread throughout an application, or perish. This causes difficulties interacting with open source or other unsigned, third party components.
  • Version coupling. Since an assembly's strong name includes its version, the .NET framework generally requires that, if assembly A is using assembly B, the exact version of assembly B against which A was linked must be available at runtime.
  • Compatibility with the GAC. The global application cache only accepts strong named assemblies.

Thanks to the advantages of strong naming, Red Gate tends to strongly name its assemblies.

To tangent slightly before weaving this story together into a coherent whole, .NET Remoting is in many ways another Good Thing. A simple, distributed .NET application is far simpler to set up and work with than some other approaches: I come from a background involving distributed DCOM applications, so I know whereof I speak.

The ability to choose whether objects passed back between tiers are executed remotely via a proxy, or are transmitted across the network and executed locally, is simple and logical. Derive from MarshalByRefObject or add the Serializable attribute to your class.

You still have to contend with one basic problem: in a client/server architecture based on Remoting, the client and server have to have a common set of types. There are several ways to achieve this:

  • Ensure that each client has a reference to the server assembly, and that the server assembly is installed locally on each client purely for access to its metadata. When the client needs types, it pulls them from the server assembly.
  • Since in the case of well known (ie. server activated) MarshalByRefObjects the client only needs metadata about the remote object in order to synthesize a proxy, a utility such as Soapsuds can be used to generate a metadata-only assembly for the client. This does require that HTTP/SOAP is your channel of choice.
  • Put all common types into a separate, common assembly. Provided interfaces are used, no server-side code needs to exist in this assembly: the common assembly simply defines the interface which the server implements, and provides enough metadata for clients to use it.

Let's consider a realistic deployment scenario. We're writing a client/server application using .NET Remoting. Both parts, client and server, are to be shipped out to users. They install the server in one place and the clients in several places.

Now let's introduce one more realistic factor to spice things up: we're going to need to roll out new versions of both client and server at some point. And let's say that our build system rolls a new assembly version number each time it builds any assembly.

With that in mind, let's review the above options. The first one is ugly, even if it were likely to fly. The second one is even grubbier, requiring additional build process in order to achieve a notional separation, and places techincal limitations on our communication protocol. The third one has to be the way to go.

It should also fit with our stated scenario. Provided none of the types in our common assembly change, we should be free to issue updates to the client and the server independently. (If the common assembly needs to change, then we need to issue updates to both client and server, and this will be a breaking change for existing customers unless they update both client and server. But this should be a rather rarer occurrence than, say, a patch containing purely UI-related client fixes, or a couple of server fixes, or both.)

The problem we encounter is this: by default, given the constraints I've outlined, IT DOESN'T WORK.

Sorry, I went a bit forum troll esque there with my caps key. But this topic has been vexing me all day, and I've only now managed to find a solution that has any chance in hell of working.

The problem one encounters is this: as soon as there is a difference between the strong name of the common type library on the client, and the strong name of the common type library on the server, everything breaks. Remoting throws exceptions as soon as any notable client/server communication starts.

And given that the strong name of an assembly includes its version number, and that we revise our assembly version numbers each build, this is a bit of a showstopper. (Even if we only rolled a new assembly version number when new features came in, it would still be a showstopper. An old client should be able to connect to a new server, provided interfaces are backwardly compatible.)

One cause of this applies only to Serializable types. By default, serialization for remoting always includes the version number of the type in question, which corresponds to the version number of its containing assembly. Since the build numbers differ between client and server, we hit exceptions upon deserialization. The fix for this is to instruct .NET remoting not to include version numbers when serializing. This is easily done by supplying includeVersions = false to the formatter sink provider associated with the channel being used for client/server communication.

The other, and more irritating cause of this occurs when a type comes across the wire by other means: for example, when an argument or return value supplied from/to a well known object is of a custom type belonging to the common assembly. (Oddly it doesn't seem to occur when instantiating a well known, server-activated object; just when calling its properties and methods with common types.) It also occurs if you pass an instance of the Type class across, and that type came from eg. typeof(ICommonInterface).

This is less easy to resolve. .NET is, in some ways, legitimately flagging a potential problem: the client and the server "do not agree" on the set of types used for communication. I enquote "do not agree" because .NET's idea of "agreeing" is: "types' strong names exactly match", which is clearly extraordinarily unlikely in any realistic situation. But by buying into strong naming and remoting, we buy into .NET's over-simplistic world view.

The way to resolve this is to take the situation out of .NET's hands entirely, just as per the serialization versioning issue, but more so. At the lowest level, these issues come down to assembly load resolution. When a remoting client receives a type over the wire from the server which belongs to strongly named assembly, .NET is wired up to attempt to load that specific assembly version to work with that type. Likewise when the server receives a type from the client.

Let's get concrete here. Say we have Client.exe version 1.0.0.1 using Common.dll version 1.0.0.1. Remotely we have Server.exe version 1.0.0.20 using Common.dll version 1.0.0.20. We need the Client to work with types from Common.dll version 1.0.0.20, and the Server to work with types from Common.dll version 1.0.0.1. This is because we know, behind the scenes, that the types are actually identical in both versions of Common.dll. No breaking changes were made to the client/server interfaces.

One thing we can do is to set up "binding redirects". We can explictly tell .NET to replace references to certain versions of an assembly with a specific version. So in the client we could have a Client.exe.config which looked like this:

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="Common"
                              publicKeyToken="deadbeefdeadbeef"
                              culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535"
                             newVersion="1.0.0.1"/>
         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>

And similarly on the server, except with newVersion="1.0.0.20".

This works, but the problem with it is that every time the version of Common.dll on the client changes, we have to roll a new Client.exe.config file with the correct "newVersion" number in it. There's no way to say newVersion="any" or similar.

A rather funkier solution I came across today is to handle this in code, rather than with configuration files. Ben Hall points out in his blog, http://blog.benhall.me.uk/2006/08/appdomaincurrentdomainassemblyresolve.html, that it's possible for Client.exe to handle the AppDomain.AssemblyResolve event, and manually resolve which assembly gets loaded. So we can say to .NET "ah, you want version 1.0.0.20 of the Common assembly? Well here's the version of that assembly I've got in my local directory, ignore the version number, take it and shut up, it's good for you."

Since types are loaded when they're first referenced, if you register for this event as the very first thing inside your Main function, you can handle all assembly versioning issues this way:

static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler
( CurrentDomain_AssemblyResolve );

    Startup();

    Application.Run( new MainForm() );

    Shutdown();
}

static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
{
    try
    {
        MessageBox.Show( "Asked for:\n" + e.Name, "Client" );
        string[] assemblyDetail = e.Name.Split(',');
        string assemblyBasePath = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        Assembly assembly = Assembly.LoadFrom( Path.Combine( assemblyBasePath, assemblyDetail[0] + ".dll" ) );
        MessageBox.Show( "Supplying:\n" + assembly.GetName(), "Client" );
        return assembly;
    }
    catch (Exception)
    {
        return null; // fail
    }
}

Which is very splendid. And admittedly, terribly terribly evil. We just bypassed the simple-minded sanity which .NET is trying to inflict on our remoting of types from strongly named assemblies, and introduced much more flexible chaos. Now it's up to us to roll enough version control of our own that the whole shebang doesn't explode and shower our users with unseemly goup. But this way we at least have a chance of updating our client and server independently, without forcing clients to patch them in lock step.

There is another solution to this whole mess of course: Use a proper language. I mean, reroll a simpler in house remoting mechanism that works. Damn, two Freudian slips, I mean: stop using strongly named assemblies. Depending on your situation, as Craig points out in his blog, http://www.pluralsight.com/blogs/craig/archive/2005/03/11/6653.aspx, this may be a far simpler solution to your woes.

-

"...I wou'd have you Observe what was told to us aboute tak'g Care whom to calle upp, for you are Sensible...and can judge how truely that Horrendous thing is reported.

I say to you againe, doe not call up Any that you can not put downe; by the Which I meane, Any that can in Turne call up Somewhat against you, whereby your Powerfullest Devices may not be of use. Ask of the Lesser, lest the Greater shal not wish to Answer, and shal commande more than you."  --  H P Lovecraft

Comments

 

RobertChipperfield said:

"There's no way to say newVersion="any" or similar."

Maybe... but you can get what's probably sufficiently close. You can actually specify a range of values for oldVersion, which is probably sufficient (i.e. oldVersion="1.0.0.0-1.0.0.9999") for most situations.
August 31, 2007 2:48 AM
 

Dan J Archer said:

Quite true Rob, and if you notice I did so in the above example. That solves half of the problem, but it does mean that every, client or server side update has to suitably munge the local application configuration file. This probably means such munging needs to be part of the build process for the application, which is all just too much tedious grind for me given the constraints of the problem. It's also tedious when testing development builds before a full installer has been created. I simply wish to say to .NET: "You see this DLL here? This is the one you want.". Ben Hall's solution, whilst drastic, does achieve this without special build- or deployment-time activities.
August 31, 2007 5:28 AM
 

RobertChipperfield said:

Sorry, I missed that in the article.

I guess I can see the point of the constraint, in that it does guarantee you haven't changed the interface and so on, but you do then get the resulting pain.

That said, I've found many other things about .NET remoting equally irritating (just you try combining unencrypted and encrypted channels in one application - then you're in for some fun!)
August 31, 2007 5:32 AM
 

djessop said:

You state that "This works, but the problem with it is that every time the version of Common.dll on the client changes, we have to roll a new Client.exe.config file with the correct "newVersion" number in it. "

Why not use policy files? (http://www.ondotnet.com/pub/a/dotnet/2003/03/17/bindingpolicy.html ) This is actually what we use where I work and the policy file works without a problem.  Since the DLL is being recompiled with a new version number, creating a new policy file can be done at the same time.  When one goes in the GAC, so does the other.

Is it a perfect solution?  No, not even close, but it does resolve a number of issues, such as having to constantly update the config file for the EXE.
September 4, 2007 12:49 PM
You need to sign in to comment on this blog

















<August 2007>
SuMoTuWeThFrSa
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678
Virtual Exchange Servers
 Microsoft now supports running Exchange Server 2007 in server virtualization environments, not just on... Read more...

Virtualizing Exchange: points for discussion
 With the increasing acceptance of the use of Virtualization as a means of providing server... Read more...

Encouraging .NET Reflector Add-ins
 Jason Haley is well-known for the resources he's provided to developers who wish to extend Reflector's... Read more...

Using .NET Reflector Add-ins
 .NET Reflector by itself is great, but it really comes into its own with the help of some add-ins. Here... Read more...

Unique Experiences!
 You'd have thought that a unique constraint was an easy concept - Not a bit of it; it can cause a lot... Read more...