Skip to main content

Outline

At a glance
  • 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.

public class InventoryService : IInventoryService { private readonly IContentLoader _contentLoader; private readonly ILogger<InventoryService> _logger; public InventoryService(IContentLoader contentLoader, ILogger<InventoryService> logger) { _contentLoader = contentLoader; // Injected by the .NET DI Container _logger = logger; } public async Task<int> GetStockLevel(ContentReference productLink) { _logger.LogInformation("Checking stock for content {ID}", productLink.ID); var product = _contentLoader.Get<ProductPage>(productLink); return product.QuantityInStock; } }

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.
Technical Warning

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

public void ConfigureServices(IServiceCollection services) { // High-scale configuration using IOptions services.Configure<ExternalApiOptions>(_configuration.GetSection("ExternalApi")); // Registering a custom service with a Scoped lifetime services.AddScoped<IInventoryService, InventoryService>(); // Optimizely core registration services.AddCms(); }

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.

public InventoryService(IOptionsMonitor<ExternalApiOptions> options) { // options.CurrentValue is always reconciled with the latest environment overrides var currentSecret = options.CurrentValue.ApiKey; }

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.