This post describes how we came to using binary package management. In the next part I’ll get into NuGet.
In the beginning, there was one repository and it held all the projects for The Motley Fool, and it was good. There were around a dozen asp.net web projects, a smattering of service and console apps, and a bunch of class libraries to hold shared code. There was one Solution (sln) to rule them all.
As time went on, we found that there are downsides to the one-giant-solution approach to .net development:
- Big Ball of Mud
- Slow builds
- Tight coupling
- Configuration hell
- Hard to release different applications on different schedules
Typically our larger applications would be split into several projects following a typical N-tier layered architecture:
- Data Access
Despite our attempts to encapsulate data access and domain logic behind the service project, code ended up leaking out to the point where domain projects were using types and methods from unrelated domain projects. Cats and dogs were sleeping together.
Around this time Steven Bohlen presented a talk to the Washington DC Alt.NET User Group titled “Domain Driven Design Implementation Patterns in .NET”. While some of us were already familiar with concepts of DDD, this talk lit a spark for us to try fixing our big ball of mud.
In late 2010 we started to make some changes. Instead of having one giant repository, shared code would be split out into separate repositories. We also took this opportunity to introduce a new project organization and architecture.
We established one repository to hold utility code, broken into specific class libraries:
- Fool.Abstractions - similar in spirit to System.Web.Abstractions; adds interfaces and wrappers to various FCL types that lack them
- Fool.Lang - similar in spirit to Jakarta Commons Lang; adds general utility classes and methods not found elsewhere
- Other projects that extend 3rd party class libraries to make them easier for us to work with in standardized ways.
Then we established another repository to hold Domain Driven, er, Domains. For example, many of our applications and web sites deal with stock market data, so one of our business domains is Quotes. In the Quotes Domain we have these projects:
- Fool.Quotes - contains service interfaces and value types; serves as an API to the domain
- Fool.Quotes.Core - contains domain logic, models, and entities; serves as a private implementation
- Fool.Quotes.Web.Api - exposes Fool.Quotes interfaces over a RESTful web API
The key to keeping our domains distinct and decoupled is to keep Core projects private. While Core is required at runtime, it should never be referenced at compile time. To bridge the gap, we use Dependency Injection to provide concrete implementations.
Domains may depend on other domains provided that they consume each other through the API project. That way entities and business logic are kept focused on their own concerns and don’t leak out to other problem areas where they don’t fit.
Gluing It Together
Having projects split into different repositories and different solutions meant that we couldn’t simply have one mega Solution that includes everything. That’s by design, so good on us. But this introduces a problem in that we still need to reference code from our utility projects and DDD projects in our applications. The first solution we came up with to handle this problem was to use the AssemblyFolders registry to have our libraries appear in the Add Reference dialog. Then to solve the runtime dependency on our private Core assemblies, we install those to the GAC so they can be loaded using reflection by our IoC container.
This approach worked fine, mostly. But we encountered some downsides after using it for a while:
- Need to have all library code checked out and built on each development machine
- No built-in way to manage different versions of the same dependency
- GAC considered harmful
- Hard to debug build errors and runtime errors
Using Continuous Integration means we’re producing new builds dozens of times a day, so it isn’t practical for us to manage different assembly versions for each build. Like most shops, we leave our assembly versions at 18.104.22.168 despite injecting actual version information into the AssemblyInformationalVersion attribute.
In order to support parallel development, we needed to find a more flexible way of managing dependencies, and at this point we started to look at binary package management.