"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