Chapter 4
What to Inject and What Not to Inject
Not everything needs to be abstracted away and made pluggable through dependency injection. Understanding which dependencies to inject and which to use directly is crucial for maintaining a balance between flexibility and unnecessary complexity. Dependencies generally fall into two distinct categories:
Stable Dependencies pose no threat to an application's modularity and typically don't require injection. They include most Base Class Library (BCL) types like String, DateTime, and Math, as well as specialized libraries with deterministic algorithms. These dependencies share several key characteristics:
• They're already fully implemented and thoroughly tested
• They follow semantic versioning and won't introduce breaking changes
• They contain deterministic algorithms with predictable outputs
• You never expect to replace, wrap, decorate or intercept them
• They're readily available across all development environments
• Examples include System.String, System.Math, encryption algorithms, and basic data structures
Volatile Dependencies are the focal point of Dependency Injection and require Seams (points of abstraction). These dependencies warrant careful consideration and typically should be injected. They include:
• Components that require setup and configuration of a runtime environment
- Database connections and repositories
- Message queues and event buses
- External web services and APIs
- File system operations
• Features that are still under development or not yet implemented
• Services that aren't installed on all development machines
• Components containing nondeterministic behavior
- Random number generators
- DateTime.Now
- GUID generators
• Resources that require different implementations across environments
- Logging systems
- Authentication services
- Configuration providers
When evaluating whether to inject a dependency, consider:
• Will this component need to be replaced in different contexts?
• Does it require complex setup or configuration?
• Will it need different implementations for testing?
• Does it involve external resources or services?
• Is it likely to change or evolve over time?
If the answer to any of these questions is yes, the dependency is likely volatile and should be injected. Remember that over-abstraction can lead to unnecessary complexity, while under-abstraction can result in rigid, hard-to-test code. The goal is to find the right balance for your specific application needs.
Chapter 5
The Three Dimensions of Dependency Injection
Dependency Injection (DI) represents a fundamental shift in how objects interact, extending far beyond the basic concept of separating responsibilities into distinct classes. When a class embraces DI, it effectively relinquishes control in two critical areas: the ability to select specific implementations of its dependencies and the management of their lifecycles. This surrender of control, while initially counterintuitive, enables powerful architectural patterns.
Object Composition, the first dimension, serves as the foundational motivation for DI. It enables applications to be assembled from individual components much like connecting electrical devices to power outlets. This pluggable architecture creates several key benefits:
• Extensibility through the ability to swap implementations without modifying existing code
• Late binding, allowing implementation decisions to be deferred until runtime
• Parallel development where teams can work independently on different components
• Easier testing by substituting real dependencies with test doubles
• Reduced coupling between components, making the system more maintainable
Object Lifetime management, the second dimension, becomes crucial when classes delegate control of their dependencies. The consuming class remains deliberately ignorant of whether its dependencies are:
• Transient (new instance every time)
• Scoped (shared within a specific context)
• Singleton (shared across the entire application)
This abstraction of lifetime management allows the system to treat all implementations uniformly, adhering to the Liskov Substitution Principle. It also enables sophisticated resource management strategies and optimization of memory usage without impacting the consuming classes.
Interception, the third dimension, provides a powerful mechanism for modifying or enhancing dependencies before they reach their consumers. By programming strictly against interfaces, we can wrap original implementations with decorators that add functionality while maintaining the Single Responsibility Principle. This technique enables:
• Security checks and authorization
• Logging and monitoring
• Caching implementations
• Performance monitoring
• Exception handling
• Validation logic
• Transaction management
The Decorator pattern becomes particularly valuable for implementing Cross-Cutting Concerns without violating SOLID principles. For example, a logging decorator can wrap any service interface to add logging capabilities without modifying the original service implementation or its consumers. This separation of concerns keeps the codebase clean and maintainable while allowing for flexible composition of behaviors.
Chapter 6
The Perils of Tightly Coupled Code
Despite understanding the theory of loose coupling, developers often fall into writing tightly coupled code. Consider Mary Rowan, an experienced .NET developer building an e-commerce application with a three-layer architecture.
Mary starts with the data layer, creating a Product class and CommerceContext that inherits from DbContext. However, she tightly couples the data access configuration by having CommerceContext load its connection string directly from a configuration file.
In the domain layer, she creates a ProductService that directly instantiates CommerceContext in its constructor, tightly coupling the domain layer to the data access layer. Following poor advice from a colleague, she designs the service to accept a boolean parameter indicating whether a customer is preferred, rather than handling this determination itself.
Finally, in the UI layer, she implements an ASP.NET Core MVC application where the controller directly depends on both domain and data access layers.
Despite her intention to create a layered architecture, Mary has created tightly coupled code. The UI layer depends on both domain and data access layers, suggesting it could bypass the domain layer entirely. While replacing the web UI with a WPF UI would be possible, replacing the data access layer would require a complete rewrite because both UI and domain layers directly depend on it.
The root problem is that the domain model directly depends on the data access layer. The ProductService instantiates CommerceContext directly, creating a hard-coded dependency that cannot be intercepted or replaced. This tight coupling prevents late binding, extensibility, maintainability, parallel development, and testability - all key benefits of dependency injection.
Chapter 7
Rebuilding with Loose Coupling
To fix these issues, we need to invert the dependency between the domain and data access layers so that data access depends on domain rather than vice versa. This inversion allows the data access layer to be replaced without rewriting the application.
Starting with the UI layer, we create a cleaner MVC application with proper view models. The FeaturedProductsViewModel contains ProductViewModel objects with a SummaryText property that encapsulates formatting logic, eliminating the casting and formatting that cluttered Mary's original view.
For the domain model, we implement POCOs and interfaces. DiscountedProduct is a simple POCO with Name and UnitPrice properties, while IProductRepository serves as an abstraction for data access. ProductService implements IProductService and uses Constructor Injection to receive IProductRepository and IUserContext dependencies.
This approach inverts the dependency between domain and data access layers. IProductRepository is defined in the domain layer but implemented in the data access layer. The domain layer now works only with types defined within itself or stable BCL dependencies, making it independent of external concerns.
The Dependency Inversion Principle states that higher-level modules shouldn't depend on lower-level modules; both should depend on abstractions. In our application, ProductService and SqlProductRepository both depend on the IProductRepository abstraction, which is owned by the domain layer.
In the data access layer, SqlProductRepository implements IProductRepository using Entity Framework Core. Unlike Mary's implementation, CommerceContext is now just an implementation detail hidden within this layer. The SqlProductRepository takes CommerceContext as a dependency through constructor injection.
Finally, we implement AspNetUserContextAdapter in the UI layer, adapting the framework-specific HttpContextAccessor to work with the domain's IUserContext abstraction. This keeps the domain and data layers framework-agnostic.
The application is composed in the Composition Root where the complete object graph is created. The HomeController receives ProductService, which itself takes SqlProductRepository and AspNetUserContextAdapter as dependencies. This creates a clean dependency chain where each component depends only on abstractions.
Chapter 8
The Power of Composition Root
The Composition Root pattern establishes a fundamental principle in software architecture: object graphs should be composed as close as possible to the application's entry point. This pattern emerges naturally as classes implement Constructor Injection, effectively pushing dependency creation upward through the application stack until reaching a single, logical composition point. This centralization of dependency resolution provides a clear, maintainable structure for managing object relationships.
For ASP.NET Core web applications, the Startup class serves as an ideal Composition Root. Within this class, the CommerceControllerActivator implements IControllerActivator to orchestrate the creation of controller instances and their complete object graphs for each incoming HTTP request. This implementation creates a clean separation between configuration management and object composition, ensuring that the composition logic remains independent of the configuration system. The Startup class handles both the registration of dependencies and their lifetime management, making it easier to maintain and modify the application's dependency structure.
A significant advantage of the Composition Root pattern is its ability to maintain a clear separation of concerns. Components throughout the application remain focused on their core responsibilities, while the composition logic is centralized in one location. This separation makes the codebase more testable, as dependencies can be easily substituted during unit testing without affecting the rest of the application.
One common misconception about the Composition Root pattern is the fear that it leads to an overwhelming concentration of dependencies at the application's entry point. However, empirical evidence shows that when comparing dependency graphs between tightly coupled and loosely coupled applications, tightly coupled code actually results in a higher total number of dependencies due to transitive relationships and hidden dependencies. The Composition Root pattern, combined with loosely coupled code, effectively reduces the overall dependency count by making relationships explicit and manageable.
The pattern also facilitates better dependency lifecycle management. By centralizing object composition, developers can more easily control object lifetime scopes, ensuring proper resource management and preventing memory leaks. This centralization makes it simpler to implement complex dependency resolution scenarios, such as dealing with circular dependencies or managing object pools.
For larger applications, the Composition Root can be organized into multiple logical sections or modules, each responsible for composing a specific subset of the application's object graph. This modular approach maintains the benefits of centralized composition while keeping the code organized and maintainable as the application grows.
Chapter 9
Constructor Injection: The Heart of DI
Constructor Injection guarantees that necessary Volatile Dependencies are always available to a class by requiring all callers to supply these dependencies as parameters to the class's constructor. This pattern statically defines the list of required dependencies, clearly documenting what the class needs to function.
The class requiring dependencies must expose a single public constructor that takes instances of all required dependencies as arguments. These dependencies should be stored in private readonly fields after validation through Guard Clauses that prevent null values.
Constructor Injection should be your default choice for DI. It addresses the most common scenario where a class requires one or more Dependencies without reasonable Local Defaults available. A Local Default is a default implementation that originates in the same module or layer, as opposed to a Foreign Default from a different assembly which would create tight coupling.
For example, to implement currency conversion functionality, we create a Currency class and an ICurrencyConverter abstraction. Since ICurrencyConverter likely represents an out-of-process resource like a web service or database, Constructor Injection is appropriate. The converter is added as a constructor parameter alongside existing Dependencies, with proper guard clauses ensuring none are null.
Chapter 10
Managing Object Lifetimes
When embracing DI, we must fully let go of control over our Dependencies, allowing the Composer to determine not just which implementation to use but also when objects are created and destroyed. This introduces the concept of Lifestyle - a formalized way of describing the intended lifetime of a Dependency.
The three most common Lifestyles are:
Singleton Lifestyle means a single instance is perpetually reused within the scope of a single Composer. Unlike the Singleton design pattern, which provides global access through static members, a Singleton-scoped Dependency is only accessible through injection. This approach generally consumes minimal memory and is efficient.
Transient Lifestyle creates a new instance every time a dependency is requested. While the Transient Lifestyle is the safest choice, it's also one of the least efficient, potentially creating numerous instances that must be garbage collected when a single instance would suffice.
Scoped Lifestyle creates at most one instance per defined scope, behaving like a Singleton within that scope but not sharing instances across different scopes. This approach is particularly valuable in web applications where requests execute concurrently but each individual request operates sequentially.
Applying Lifestyles incorrectly can lead to serious problems, including Captive Dependencies (keeping Dependencies referenced beyond their expected lifetime), Leaky Abstractions (exposing Lifestyle choices to consumers), and per-thread Lifestyle (causing concurrency bugs by tying instances to thread lifetimes).
Chapter 11
Implementing Cross-Cutting Concerns with Interception
Interception allows us to enhance or transform implementations without changing their core functionality - similar to how a simple veal cutlet can be transformed into a complex dish through various additions. When programming to interfaces, you can wrap core implementations in other implementations of the same interface, allowing you to modify behavior.
The Decorator pattern attaches additional responsibilities to objects dynamically by wrapping one implementation of an Abstraction in another implementation of the same Abstraction. Multiple Decorators can be nested to create a "pipeline" of interception.
Cross-Cutting Concerns are aspects that affect multiple parts of an application across different modules or layers. Common examples include auditing, logging, performance monitoring, validation, security, caching, error handling, and fault tolerance.
The Circuit Breaker pattern adds fault tolerance to applications communicating with out-of-process resources that may become unavailable. Rather than repeatedly attempting to connect to a failing resource and blocking threads with timeouts, the Circuit Breaker "trips" after detecting failures, causing subsequent calls to fail fast.
Security can be implemented by intercepting method calls and checking if the current user has the required role before allowing operations to proceed. The SecureProductRepositoryDecorator implements authorization by intercepting method calls and checking user roles before allowing operations.
Chapter 12
Aspect-Oriented Programming: Taking DI Further
Aspect-Oriented Programming (AOP) addresses the efficiency and maintainability challenges when implementing Cross-Cutting Concerns. While Decorators successfully delegate implementation details to separate interfaces, they often violate the DRY principle with repetitive code.
The SOLID principles provide a framework for creating maintainable code that supports AOP:
The Single Responsibility Principle ensures every class has only one reason to change.
The Open/Closed Principle states classes should be open for extension but closed for modification.
The Liskov Substitution Principle requires that all Dependency consumers observe the principle when invoking Dependencies.
The Interface Segregation Principle promotes using fine-grained Abstractions rather than wide ones.
The Dependency Inversion Principle states that code should program against Abstractions, with the consuming layer controlling the shape of consumed Abstractions.
By applying these principles, we can refactor problematic designs like a monolithic IProductService interface with dozens of methods. The solution involves separating reads from writes, splitting interfaces and implementations, introducing Parameter Objects, and using generic abstractions like ICommandService<TCommand>.
This refactoring transforms the domain layer by replacing a monolithic service interface with a command-based approach, making commands explicit artifacts in the system with a common API. The command pattern enables effective application of cross-cutting concerns without code duplication, dramatically improving maintainability.
Chapter 13
Conclusion: The DI Container Decision
While DI Containers like Autofac, Simple Injector, and Microsoft.Extensions.DependencyInjection can automate object composition and lifetime management, they're optional tools that require careful consideration. Each container offers unique features - Autofac provides robust module support, Simple Injector emphasizes performance and verification, and Microsoft.Extensions.DependencyInjection delivers seamless ASP.NET Core integration. The decision to use a container versus Pure DI depends on project size, team experience, and risk assessment.
Pure DI's strong typing allows the compiler to provide immediate feedback about correctness, catching configuration errors at compile-time rather than runtime. However, its maintenance burden grows linearly with application size. A project with 50 components might require 200 lines of composition code, while 500 components could necessitate 2000 lines. This scaling factor makes Pure DI increasingly complex for larger applications.
DI Containers offer Auto-Registration through Convention over Configuration, creating a set of conventions that your code adheres to. For example, you might configure all classes ending in "Repository" to be registered as their corresponding interface, or register all services in a particular namespace with a specific lifetime scope. These conventions can reduce hundreds of manual registrations to a few lines of configuration code, minimizing Composition Root maintenance.
Pure DI works best for small to medium Composition Roots, particularly in scenarios where compile-time safety is paramount or when working with security-sensitive applications where container behavior must be completely deterministic. Auto-Registration shines for larger applications with many classes that can be captured by conventions, especially in enterprise scenarios with dozens of similar components following consistent patterns.
When implementing a DI Container, it's crucial to maintain proper architectural boundaries. The container should be limited to the Composition Root to prevent applying the Service Locator anti-pattern. This means avoiding container access in business logic, controllers, or service classes. Instead, dependencies should flow through constructor injection, maintaining clear dependency graphs and preserving the benefits of dependency injection.
The choice between Pure DI and container-based DI isn't permanent - you can start with Pure DI and introduce a container later if needed, or vice versa. Some teams even use hybrid approaches, using Pure DI for core components while leveraging container features for peripheral systems.
By mastering dependency injection principles, practices, and patterns, you'll create software that's more maintainable, extensible, and testable - software that can adapt to changing requirements without requiring extensive rewrites. This architectural approach promotes loose coupling, high cohesion, and clear separation of concerns. The true promise of dependency injection lies in creating sustainable software architecture that stands the test of time, whether implemented through Pure DI or with container support.