The tenet is: reduce the number of your .NET assemblies to the strict minimum. Having a single assembly is the ideal number. This is for example the case for Reflector or NHibernate that both come as a single assembly.
A lot have been said on this topic. I dig in this articlewhy using more namespaces and less assemblies for componentization is a good thing. Jeremy Miller does it so in this blog post. The point is that assemblies are physical while namespaces are logical. As a consequence, having N assemblies multiplies by N the burden of dealing with physical things. This burden consists of referencing N assemblies from a Visual Studio project, slowing down significantly compilation of N Visual Studio projects, managing the deployment for N files, need for the CLR to do N CAS security checks at startup time, etc… Personally I have seen applications with up to 750 assemblies! Quote from Jeremy Miller: I took 56 projects one time and consolidated them down to 10-12 and cut the compile time from 4 minutes to 20 seconds with the same LOC
I would like to discuss here the motivations behind creating an assembly. I will then illustrate these reasons through the choices we did for assemblies of the NDepend code base. In an effort to reduce the number of assemblies in your shop, it is a good thing to find a valid reason for the existence of each assembly of your code base. If no solid reasons can be found you’ll find room for merging assemblies.
Valid and Invalid reasons to create an assembly
Valid reasons to create an assembly:
- Tier separation: Need to run some different pieces of code in different AppDomain or process. The idea is to avoid overwhelming the precious Window process memory with large pieces of code not needed.
- Potential for loading large pieces of code on-demand. This is an optimization made by the CLR: assemblies are loaded on-demand. In other words, the CLR loads an assembly only when a type or a resource contained in it is needed for the first time. Here also you don’t want to overwhelm your Window process memory with large pieces of code not needed most of the time.
- Framework features separation. In case of very large framework, users shouldn’t be forced to embed every features into their deployment package. For example, most of the time an ASP.NET process doesn’t do some Window Forms and vice-versa, hence the need for 2 assemblies System.Web.dll and System.Window.Forms.dll. This is valid only for large framework with assemblies sized in MB. For example the NUnit http://www.NUnit.com framework certainly does not need 25 assemblies for 15.000 Lines of Code! Quote from Jeremy Miller: Nothing is more irritating to me than using 3rd party toolkits that force you to reference a dozen different assemblies just to do one simple thing.
- AddIn/PlugIn model, need for interface/factory/implementation physical separation.
- Test/application code separation. If you are not releasing source code but only assemblies, you likely don’t want to release tests. Not releasing test assemblies make this easy.
- When several assemblies have been created for the reasons above, they likely need to share some common code. Such shared code must be placed in a dedicated shared assembly.
We could add assembly as a unit of versioning but IMHO, the need for versioning is a subset of all these reasons enumerated above.
Invalid reasons to create an assembly:
- Assembly as unit of development, or as a unit of test. Modern Source Control Systems make it easy for several developers to work simultaneously on the same assembly (i.e the same Visual Studio project). The unit should be here the source file.
- Automatic detection of dependency cycles between assemblies by MSBuild and Visual Studio. There are tools such as NDepend that can detect dependency cycles between namespaces or types of an assembly.
- Usage of internal visibility to hide implementations details. This public/internal visibility level is useful when developing a framework where you want to hide implementation details to the rest of the world. Your team is not the rest of the world, so you don’t need to create some assemblies especially to hide some implementations details.
- Usage of internal visibility to prevent usage from the rest of the application. If you want to prevent usage and thus control the structure/dependencies of your code base, you should better use some dedicated tools such as NDepend .
If you are interested in reducing the number of your Visual Studio projects/assemblies, I would suggest reading this blog post Hints on how to componentized existing code. It shows how to use some NDepend dependencies features to get some hints about which sets of assemblies should be merged.
A case study
NDepend code base is split across 11 assemblies and here is the dependency diagram of NDepend assemblies (made by NDepend itself):
Something you might notice is the XDepend term. We are currently building the XDepend product (aka NDepend for Java), that will be released in 2009. More information are available on the official website http://www.XDepend.com and I will talk more about this in the future. This XDepend/NDepend duality leads to 2 different deployment packages for the 2 products and this is why there are NDepend.Console.exe, VisualNDepend.exe, NDepend.Platform.DotNet.dll on one hand and XDepend.Console.exe, VisualXDepend.exe, XDepend.Platform.Java.dll on the other hand.
The need for 4 exe instead of 2 was needed mostly for terminology. XDepend.Console.exe certainly makes more sense for an XDepend user than NDepend.Console.exe. All these 4 executables are almost empty in terms of code. The console ones initialize the platform (Java or .NET) and start the analysis implemented in NDepend.Analysis.dll while the Visual ones initialize the platform and start the UI, implemented in NDepend.UI.dll.
The need to initialize the platform (.NET or Java) is a motivation for isolating the code specific to each platform. Hence the need for the 2 assemblies NDepend.Platform.DotNet.dll and XDepend.Platform.Java.dll. This separation will also make easy potential future platform support (C++, Delphi…).
The tool comes with 2 primary usages, analyze code and digging into analysis results through the UI. This is the motivation for having 2 different executable assemblies: NDepend.Console.exe and VisualNDepend.exe on one hand, and 2 different libraries, NDepend.Analysis.dll and NDepend.UI.dll on the other hand. The idea is to avoid having both these assemblies loaded inside the same process.
NDepend.Analysis.dll and NDepend.UI.dll both rely on a lot of common code, core domain objects + many helper/util code, hence the need for creating the NDepend.Framework.dll
NDepend.CQL.dll is a lightweight assembly (less than 10KB) that contains the plumbing for declaring CQL rules directly inside the source code. Not only this assembly NDepend.CQL.dll is used by NDepend users who harness this possibility, but also we use it extensively to declare our own constraints in our code. For users who which to declare CQL constraints inside their source code, it is more convenient to link with a single and lightweight assembly, hence the need for NDepend.CQL.dll.
Finally, the NDepend.AddIn.dll assembly contains the plumbing needed to register the NDepend VisualStudio and Reflector addin. This assembly references big assemblies such as the Reflector.exe assembly. In order to prevent loading by mistake Reflector.exe in one of the NDepend process, it was a good thing to create a dedicated NDepend.AddIn.dll assembly. Also, addin assemblies are registered in VisualStudio and Reflector through their names and the name NDepend.AddIn is well-suited.