Dependency Injection Patterns
Outline
- Default pattern: Constructor injection is the primary way to wire Optimizely services into controllers and custom business logic.
- Lifetime discipline: Scoped services are the safest default for CMS development; singleton misuse can become a quiet disaster.
-
Configuration strategy: Use the Options Pattern for structured settings and
IOptionsMonitor<T>when values may change at runtime. -
DXP reality: Multi-node deployments require platform-aware caching such as
ISynchronizedObjectInstanceCache, not homemade in-memory islands.
Starting with CMS 12 and continuing as the structural baseline for CMS 13, Optimizely fully aligns its service management with the ASP.NET Core Dependency Injection (DI) framework. This is a foundational shift for developers coming from older CMS versions, because it enables a service-first design that is testable, modular, and cloud-native.
In a PaaS (DXP) solution, proper DI is not optional. It is the technical glue that ensures your code respects the platform’s multi-instance scalability model and its security boundaries.
This module explores the DI patterns required for Optimizely development and how to design custom business services that are technically robust and enterprise-ready.
1. The Proactive Pattern: Constructor Injection
The most robust and recommended pattern in CMS 13 is constructor injection. The .NET DI container automatically resolves required services when a class is instantiated. This pattern is proactive because it makes dependencies visible and explicit through the class’s public construction contract.
1.1 Implementing Constructor Injection
When building a custom service or CMS controller, you simply declare the required Optimizely and .NET interfaces in the constructor. The framework handles lookup, instantiation, and lifetime management.
2. Managing Lifetimes in a Composable Stack
Choosing the correct service lifetime is essential for the stability of a PaaS implementation. If a database-heavy or request-aware service is incorrectly registered as a Singleton, the result can include cross-thread contamination, stale state, or memory retention issues.
- Transient: A new instance is created every time the service is requested. This is useful for lightweight services or when complete isolation is desirable.
-
Scoped: One instance is created per HTTP request. This is the gold standard for most CMS-facing service design. Services interacting with
IContentLoader, repositories, or the current user context are commonly scoped. - Singleton: One instance lives for the entire application lifetime. This should be reserved for truly global services or platform-managed shared infrastructure.
Never inject a Scoped service such as IContentRepository into a Singleton. Doing so causes the scoped dependency to be captured for the life of the application, which can lead to connection exhaustion, stale object behavior, and very unpleasant debugging sessions in DXP.
3. Registering Custom Services and Options
In CMS 13, Startup.cs or the relevant service registration area in your bootstrapping code acts as the central place for service wiring. For more complex configuration needs, avoid scattering raw string settings through the codebase and instead adopt the Options Pattern.
3.1 Code-First Service Registration
3.2 Advanced Consumption with IOptionsMonitor
In PaaS environments, configuration values may change without requiring a full process restart. For example, environment-driven overrides or portal-based settings may shift between deployments. In those cases, IOptionsMonitor<T> gives you live, thread-safe access to the most current values.
4. Designing for DXP Synchronization
In a DXP environment, your application runs across multiple Azure App Service nodes. That means a simple in-memory Dictionary used as a local cache can easily create inconsistent state, where one instance knows something another instance does not.
Technical requirement: If a service needs internal caching, inject ISynchronizedObjectInstanceCache. In DXP, this service is coordinated through platform-level synchronization infrastructure. When content or cache entries are invalidated on one node, the platform broadcasts the change so other nodes can remain in sync as well.
Conclusion
Dependency Injection in Optimizely CMS 13 is not just a framework convenience. It is the architectural glue that connects custom business logic to the platform’s distributed runtime. By relying on constructor injection, selecting the correct service lifetimes, and using the Options Pattern alongside platform-managed cache services, you create solutions that remain testable, modular, and safe under multi-node DXP conditions. Mastering these patterns is what separates basic service wiring from genuinely production-ready platform design.
