Thursday, January 3, 2008

Two Versions of the Same Shared Assembly

This post is a follow up to an earlier cry for help: Shared Assemblies, Components, and Applications

I actually have a few workable solutions to the problem I presented there now. But you don't have to read that old post, I'll present the problem again here in a lot more detail:

Suppose you have some .NET assemblies which depend on each other as follows:
  • App1 is an executable application
  • Comp1 is a dll library, used by App1
  • Comp2 is a different dll library, used by App1
  • SharedAsm is a dll library used by App1, Comp1, and Comp2
Notice that the diagram indicates that there are three different versions of SharedAsm being referenced here. The problem I have been trying to solve is, how would you set up such a system with .NET?

Lets step back first and look at why you might run into this. It's quite simple. Component1 and Component2 are pretty beefy modules. So beefy in fact that they each have their own development teams. The components aren't just used by App1 either. There are dozens of applications that use these components. This is a perfectly common occurrence. The only reason it is a problem is because of the dependency on SharedAsm.

Here's why: Comp1 is working with version 1 of SharedAsm. They want to release their component to the App1 development team. But App1 is using version 3 of SharedAsm. Version 3 is not compatible with version 1. If we're working in Visual Studio here this will be a serious problem.

Suppose App1 is setup with a solution that has all project references. If this is the case, Comp1 wont compile because it was expecting version1 of SharedAsm but its going to find the current source control version instead.

Okay, suppose App1 is setup with a solution that references dlls instead. This still wont work. When the dlls are copied into the output directory one version will overwrite the other version... Boom.

It seems that the only option is for the Comp1 team to update their code to use the same version of SharedAsm as App1 is using. But what if App2 is using a different version? Well, lets just make it company policy that all active projects must use the latest versions of all assemblies. But what about App3 which is no longer in active development and doesn't have a development team anymore?

Let's sum up our troubles:
  • Comp1 can only be used in any application as a project reference if that application will reference ALL assemblies as projects and be willing to immediately update to new versions when they are checked in (sloooooooow build times w/ all those projects, very fragile environment as the slightest bad checkin breaks everyone, hard to deal with legacy projects)
  • Comp1 can't be "released" as dlls because of the potential for shared assembly version conflicts
  • We can't work on non active projects without updating them to the newest versions of all components or performing fragile and complicated branch magic in our source control system.
Are there any completely automatic solutions to this problem supported by Visual Studio? Nope.

Are there any solutions for this at all? Lucky, yes.
  1. Add the version number to the end of the dll names by changing the Assembly Name property of the visual studio project before building it.
  2. Use ILMerge to combine the SharedAsm.dll and Comp1.dll into a single Comp1Merged.dll using the /internalize option.
  3. Install SharedAsm to the GAC and make sure all references to it in the Visual Studio projects are set to SpecificVersion = true and CopyLocal = false.
I think the first solution, appending the version number to the name, is the best all around solution.
  • + It can be done from within Visual Studio requiring no outside tools or custom build scripts
  • + It generates pdbs with no effort allowing people using the assembly to step through the code at run time
  • + Its memory efficient, different versions of SharedAsm will only be loaded if different versions are actually being used
  • + The SharedAsm team only has to add the version number. Nothing unusual has to be done by any other teams.
  • - It requires someone to manually update both the version number and the assembly name
  • - It requires that SharedAsm go through "structured releases" in which the version number is incremented and the dlls are made available to anyone who wants or needs to upgrade
  • - To upgrade, teams using SharedAsm have to both remove the old dlls from their project and update the references (this can be automated with a script if necessary) (and fix errors)
The second solution, ILMerge, works great, but it has a few more serious downsides. For more details on this solution refer to this post on Jon's ReadCommit blog.
  • - Requires an outside build script to run ILMerge (or a post build event in VS? or a project file modification?)
  • + PDB files are merged as well
  • -- Potentially very memory inefficient as n versions of SharedAsm will be loaded regardless of if they are actually different versions. This is a deal breaker for the way I need to use this pattern as I have will have way more than 2 components using the same shared assembly.
  • +/- The process is managed by the Comp1/2 development teams, the SharedAsm team doesn't have to do anything special.
  • - It requires that SharedAsm go through "structured releases" in which the new dlls are made available
  • + To upgrade teams using SharedAsm just copy the new version over the old version (and fix errors)
The third solution, GAC, also works great but requires the most work and maintenance.
  • - Dlls must be placed in the GAC on every developer's computer
  • - Comp1 team and Comp2 team must release script to add needed dlls to GAC as well as new version of their comp1/2.dll
  • - No PDBs for dlls in the GAC, thus can't step through code in SharedAsm when used by components
  • - The process requires effort from everyone
  • + As memory efficient as the version number solution
  • - Dlls must be placed in the GAC, regardless of if they really make sense there
I have tested all of these methods and they do all work. I haven't actually started using them in our development environment so I'm only speculating about the pros and cons that will arise from that.

For the record here are some non-solutions:
  • .netmodules generated with csc.exe
  • use of the extern keyword and aliasing references (requires too much foresight and work to be practical)
  • renaming dlls after they've been built (you must change the Assembly name before you build or .NET will not be able to find the dll to load it)
  • ILMerge w/o the internalize option. This will work if App1 doesn't directly reference SharedAsm. But as soon as you add the SharedAsm reference you will receive a compiler error: The type "sharedasm.type" exists in both "comp1.dll" and "comp2.dll." This is because all three types are visible and .NET doesn't know which one you wanted to use. The internalize switch solves this problem by making the types internal in comp1 and comp2.
Big thanks to Jon and his ReadCommit blog which taught me about the /internalize switch of ILMerge.

FOLLOWUP: Versions Followup