Skip to main content

Outline

At a glance:
  • Native alignment: CMS 12 fully adopts the standard .NET Core DI container, replacing legacy structures
  • Injection hierarchy: Constructor injection is the gold standard, while Injected<T> handles property-level needs
  • Lifetime management: Proper use of Transient, Scoped, and Singleton prevents memory leaks and thread-safety issues
  • Locator avoidance: ServiceLocator is deprecated for modern development to ensure testability and architectural clarity

Optimizely CMS 12 is built upon ASP.NET Core, meaning it leverages the native .NET Dependency Injection (DI) container. Dependency injection is a fundamental architectural pattern used to achieve Inversion of Control (IoC), enabling decoupled, testable, and maintainable systems. In enterprise PaaS environments, understanding how to correctly register and consume services - while adhering to specific Optimizely patterns like Injected<T> - is essential for backend governance and system stability.

1. Core DI Framework and Service Lifetimes

Registration of custom services is primarily managed in Program.cs within the ConfigureServices method. Services must be registered with an appropriate lifetime, which determines how long an instance lives before the container disposes of it.

Service Lifetime Best Practices

Managing object lifecycles correctly is vital for performance and preventing memory leaks or race conditions:

  • Transient: Created every time they are requested. Ideal for lightweight, stateless services with no shared state.
  • Scoped: Created once per client request (connection). Many Optimizely services such as IContentLoader are registered as scoped or transient to ensure thread safety within a single web request.
  • Singleton: Created once on first request. All subsequent requests use the same instance. Suitable for global configurations or cache managers that maintain state across the entire application lifecycle. Never inject a scoped service into a singleton.
C#
public void ConfigureServices(IServiceCollection services) { // Adding standard CMS services services.AddCms(); // Registering custom services with specific lifetimes services.AddTransient<IMyStatelessService, MyStatelessService>(); services.AddScoped<IUserContextService, UserContextService>(); services.AddSingleton<IGlobalConfigManager, GlobalConfigManager>(); }

2. Constructor Injection (The Primary Pattern)

Constructor injection is the industry standard and the most recommended pattern in Optimizely CMS 12. It makes dependencies explicit and ensures that a class cannot be instantiated without its required services - making it impossible to misuse a class at compile time.

Technical Implementation

Dependencies are provided as constructor parameters. The .NET DI container automatically resolves them when the class - for example, a controller or service - is requested from the container.

C#
public class ContactPageController : PageController<ContactPage> { private readonly IContentLoader _contentLoader; private readonly IMyCustomService _myService; // Explicit constructor dependencies public ContactPageController(IContentLoader contentLoader, IMyCustomService myService) { _contentLoader = contentLoader; _myService = myService; } public IActionResult Index(ContactPage currentPage) { var data = _myService.ProcessPage(currentPage); return View(data); } }

3. Property Injection and Injected<T>

Property injection allows services to be injected into public properties after an object has been constructed. This pattern is used when a class has optional dependencies or when deep inheritance structures make constructor injection cumbersome.

The Injected<T> Shorthand

Optimizely provides the EPiServer.ServiceLocation.Injected<T> wrapper as a shorthand for property-level injection. The service is lazily resolved from the container when the .Service property is first accessed - no constructor parameter required.

C#
using EPiServer.ServiceLocation; public class MyCustomComponent { // Lazy-loaded property injection public Injected<IContentLoader> ContentLoader { get; set; } public void Execute() { // Resolved on first access var root = ContentLoader.Service.Get<PageData>(ContentReference.RootPage); } }

Note: Injected<T> is best reserved for base classes, static helpers, or component hierarchies where constructor injection would require threading the dependency through many layers. Prefer constructor injection wherever practical.

4. Method Injection

Method injection involves passing a dependency directly into a specific method as a parameter. In CMS 12, this is frequently seen in middleware, action filters, or IInitializableModule implementations where the dependency is only needed for a discrete operation.

Usage in Controller Actions

The [FromServices] attribute allows method-level injection in MVC controller actions, preventing constructor bloat when a service is only needed for a single action rather than the whole controller.

C#
public IActionResult Search([FromServices] ISearchService searchService, string query) { var results = searchService.Query(query); return View(results); }

5. Avoiding the Service Locator Anti-pattern

While ServiceLocator.Current.GetInstance<T>() exists for compatibility and niche static contexts such as custom validators or legacy extensions, it is considered an anti-pattern in modern Optimizely development.

  • Opaque Dependencies: It hides the dependencies of a class, making it difficult to understand system architecture at a glance or during a code review.
  • Testing Hurdles: Mocking dependencies for unit tests becomes significantly more complex when using static service location - dependencies cannot be substituted via constructor parameters.
  • Scope Mismanagement: In CMS 12 PaaS environments, using ServiceLocator.Current outside of a web request requires manual scope creation using CreateServiceLocatorScope to prevent data corruption.

Important: If you encounter ServiceLocator.Current in existing code, treat it as a refactoring candidate. In scheduled jobs or background tasks that run outside of a web request context, always create an explicit scope with CreateServiceLocatorScope rather than calling the locator directly.

Conclusion

Mastering dependency injection in Optimizely CMS 12 is critical for building robust enterprise applications. Constructor injection should be mandated as the default pattern to ensure architectural transparency and testability. Property injection via Injected<T> remains a powerful tool for reducing boilerplate in complex component hierarchies, while method injection via [FromServices] provides granular control within specific controller actions. By strictly adhering to these patterns and avoiding the ServiceLocator anti-pattern, technical teams maintain a scalable and performant PaaS footprint.