Welcome to the dependency hell
While working on a little open-source demo project, I ran into that well-known challenge of NuGet dependency management again. This little project results in a NuGet package, that on itself also relies on other packages. Now, if I would just add those dependencies to the .nuspec file using the <dependencies> element, I’m going to put a burden on the people who want to use my package. Why? Because whenever they use my package, they’ll start to depend on all the packages my package references.
If my package is the only one they use, it’s probably fine. But what if they use another package that also uses Json.NET (for instance), but they rely on an incompatible version? You can’t use two different versions of the same assembly in the same process (or more specifically, the same AppDomain). If I’m using 7.1.3, they are using 7.2.1 and the involved package uses Semantic Versioning (which Json.NET fortunately does), the NuGet Package Manager will happily select the higher of the two. 7.2 implies a backwards-compatible feature update of 7.1. But if they are using 8.1, which implies a major upgrade and potential breaking changes, the NuGet Package Manager will simply give up. Now imagine that my package has a lot more dependencies that can conflict with the dependencies of the code base that is using it. That’s what the .NET community typically refers to as “dependency hell”.
A way out?
Yes! Merge as many of the assemblies of your dependencies into the main assembly as internal types not visible to the outside world. This has some implications however. For instance, if I would merge Json.NET into my main assembly and the consuming party also uses Json.NET, at run-time, the Json classes would appear twice in the AppDomain. So even though both classes would have the same name and namespace, the CLR would treat them as completely different types. To be more specific, if I would annotate my code with the [JsonConverter] attribute and then merge the Json.NET assembly into my assembly, the other Json.NET as loaded by the consuming party wouldn’t be able to recognize the attribute.
What does that mean? Well, it means that you need to consider the circumstances before you make the decision whether or not merge a dependency. Let me help you with that by providing a couple of guidelines:
- If the types of that dependency are used on the public types of your package, you must expose that package as a NuGet package dependency.
- If the package and package consumer do have to use the same version of the dependency at run-time, use a package dependency. The above mentioned example of working with Json.NET annotated types is a good example of this.
- If the package and package consumer don’t have to use the same version of the dependency, then by all means, merge the dependency into the package. You’ll make your package consumer a happy person.
Hiding your dependencies
Obviously option 3 is the preferred option, but isn’t always possible. Sometimes you can achieve that by not directly exposing types from your internal dependencies and using smart constructs like delegates instead. For example, let’s say your internal dependency has some kind of extension point that consumers of your package would need. Something like this:
public interface IExtensionPoint
void Connect(ModuleInfo module)
Your first reaction would be to take option 1 and expose the IExtensionPoint to your consumers. But rather than that, you could also define a custom ModuleInfoAdapter class which mimics some of the properties of the ModuleInfo class and expose a delegate like this:
public delegate void Connect(ModuleInfoAdapter module);
Then when the consumer passes a method or expression into that delegate, you could internally map the exposed ModuleInfoAdapter back to the actual type expected by the merged library.
Another common example is the case where your package internally uses a library that supports selecting and configuring a specific library-provided algorithm (or Strategy) and you need to delegate both to your consumers. You could hide that detail by defining an abstraction on top of that algorithm and allowing your consumer to use some kind of Factory Method to select a particular implementation of that strategy without exposing the internal implementation of it. You’d be surprised to learn what you can do with some smart applications of the Adapter and Bridge patterns, or by simply implementing certain interfaces explicitly. This may all feel like I’m over complicating things, but anything is warranted to keep your dependencies hidden.
To merge or to repack
Within the .NET world there are currently two tools to merge multiple assemblies (and their PDBs) into a single output assembly; ILMerge and ILRepack. The former has been Microsoft’s official tool of choice, but hasn’t received a lot of love over the last years. The latter is an open-source library that has seen many new releases since it’s first inception. Which one to use? It depends. In the beginning, ILRepack lacked in support for certain edge cases, which forced us to switch back to ILMerge. On the other hand, that one didn’t properly support .NET’s portable class libraries out-of-the-box. I would recommend to try ILRepack first and see how far you’ll get with it. You can find an example of a PSake script that uses ILRepack in my FluidCaching project. Do notice that very recent versions require .NET 4.6, so using the latest and greatest may complicate your build agent requirements. Regardless, we’ve run into some weird exceptional situations that neither could handle and which forced us to expose an internal dependency as a public dependency anyhow.
So what about you? Does this all make sense? What kind of challenges did you run into while dealing with dependencies and how did you solve those? I’d love to hear your thoughts by commenting below. Oh, and follow me at @ddoomen to get regular updates on my everlasting quest for better solutions.