DotNetStarter is an open-source framework which allows application developers to install NuGet packages that can participate in an application's composition root. The composition root is a place near the beginning of the application, such as the global.asax constructor, where modules are composed together. DotNetStarter achieves this by providing interfaces and attributes which are discovered during the scan portion of the startup process. They provide the needed hooks for NuGet package authors to participate in the application's composition.
At Diagram, we take advantage of this by building many types of packages, such as 301 redirects and XML Sitemaps. These packages share a common core of .NET code, but implement certain portions of functionality geared towards a specific implementation, such as for the Episerver platform or the Umbraco content management system (CMS).
These specific implementations become top level packages that our developers can install for client implementations. NuGet will then also install all the other components required dependencies such as the common code that isn't specific to any implementation. This approach makes distributing new features and bug fixes much easier. Package authors can work in the common core code and push updates via NuGet most times without having to change the specific implementation. All of this functionality is then discovered during DotNetStarter's scan and startup process in the application, wiring all the components together, resulting simpler application configuration.
A Brief History
DotNetStarter began a few years ago when I realized an earlier project with similar goals was too brittle. The prior version relied solely on a single dependency-injection container, DryIoc. The interfaces the project provided were tightly coupled to a specific version and the release of the next version exposed the rigidness design flaw. The container abstraction being used had changed and no longer provided the needed functionality the project required.
Another flaw was relying on a single container and then trying to integrate many implementations into systems where a different dependency injection (DI) container was already defined. Implementation-specific packages became very clunky by importing core component services from two different DI containers, which frequently lead to issues as one container may be configured before another. This approach also made constructor injection a near impossible task as all the needed components were split between containers. Another major flaw was the project targeted an old version of the .NET framework due to the need to support legacy clients.
These flaws led me to what I thought was a great idea, abstracting the container registration and resolving process! I could create core functionality that is independent of a DI container and in the application the developer may choose the container to consume the service registrations. I could also take advantage of building the code to support multiple versions of the .NET framework and deliver them in a single NuGet package. I was sold on this approach and posed the idea with my team that we should share this with others by making it open-source. In doing so, we could greatly benefit as others may be inclined to use it and submit features or bug fixes.
Should I Use It?
The flexibility of this approach doesn't come without trade-offs. The great idea of abstractions in DotNetStarter created what Mark Seemann, an expert in dependency injection, calls a conforming container which he states is an anti-pattern. DotNetStarter has already experienced a few of the consequences noted in his article, namely breaking changes churn, resulting in versioning hell if dependencies are not kept up to date in participating packages. As the maintainer of both DotNetStarter and many other packages relying on it, this consequence hit me hard.
The churn led to the creation of the registration attribute package, which removes the conforming container bits, and simply allows for services implementing abstractions to announce the intention to register to a DI container with a preferred lifetime. Many developers may not agree with this approach as it allows the implementation to determine its lifetime and also makes the implementing services harder to discover. Application developers have some assistance from DotNetStarter to relieve this concern as the newest version provides a mechanism in the StartupBuilder to provide a custom implementation of a registration modifier which has access to all service implementations using the registration attribute. The modifier also provides the ability for the lifetime to be changed as needed by the application developer.
There have been other lessons learned along the way that made their way into the DotNetStarter codebase. One such lesson is to fail early with good descriptions of exceptions. In earlier versions, locator implementations would return null if a service could not be resolved, which frustrated developers. Tracking down the missing dependency involved manually resolving services one-by-one until the null occurred. A very tedious chore. Another lesson was to program against abstractions that can be changed either in the DI container, or in the startup process before DI is configured.
Overall, even though DotNetStarter is a conforming container and may be viewed as an anti-pattern, it has given us great flexibility in how we deliver core functionality to our clients in the .NET technology stack, regardless of the implementation. Application developers can get up and running with all of our components with minimal setup, but also make adjustments with inversion of control. The functionality continues to improve by adapting from the lessons learned, hopefully providing a better developer experience for those who use it. All the code is available on GitHub, and also has documentation. If you would like to help improve DotNetStarter, pull requests and feedback are always welcomed.