Skip to main content

Outline

At a glance:
  • What it is: IContentRepository is the write-capable API for CRUD operations on Optimizely content.
  • What to prefer for reads: Use IContentLoader for read-only operations to keep intent clear and reduce accidental writes.
  • Core gotcha: Content instances are effectively immutable; update via CreateWritableClone() + Save().
  • PaaS mindset: Treat content operations as versioned data changes - language, permissions, save actions, and workflow impact all matter.

Introduction

Optimizely CMS 12 provides a robust API for managing content programmatically, forming the backbone of dynamic digital experiences. At the heart of content manipulation for developers lies the IContentRepository interface, a cornerstone for interacting with the CMS content model.

This article walks through practical usage of IContentRepository and its read-only counterpart IContentLoader, with concrete examples and the operational considerations that matter in CMS 12 PaaS deployments.

1. Understanding IContentRepository and IContentLoader

The EPiServer.IContentRepository interface is the primary gateway for operating on objects that implement EPiServer.Core.IContent (pages, blocks, media, and custom types). It supports full CRUD semantics and versioning-aware save behavior.

IContentRepository vs IContentLoader

Select a topic to expand and read the details.

IContentRepository - when to use it

Read + write. Use only when you intend to create, update, delete, or move content. Using the write-capable interface for pure reads over-declares intent and opens the door to accidental mutations.

IContentLoader - when to use it

Read-only. Prefer this for queries, page rendering, navigation building, and listing content. The interface choice communicates intent clearly to reviewers and reduces the risk of accidental writes during development or refactoring.

2. Getting started: obtaining repository instances

In CMS 12, dependency injection (DI) is the recommended and most maintainable way to access IContentRepository and IContentLoader. DI keeps dependencies explicit, improves testability, and avoids hidden coupling.

2.1 Dependency injection (recommended)

C#
using EPiServer; using EPiServer.Core; public class ContentService { private readonly IContentRepository _contentRepository; private readonly IContentLoader _contentLoader; public ContentService(IContentRepository contentRepository, IContentLoader contentLoader) { _contentRepository = contentRepository; _contentLoader = contentLoader; } // Use _contentLoader for reads; _contentRepository for writes. }

2.2 ServiceLocator (alternative, use with caution)

ServiceLocator can be useful in static or legacy contexts where constructor injection is not feasible, but it hides dependencies and makes testing harder. Treat it as a last resort.

Note: If ServiceLocator is spreading across the codebase, that is usually a refactoring signal. It is convenient, but it quietly makes your architecture harder to reason about.

C#
using EPiServer; using EPiServer.ServiceLocation; var repository = ServiceLocator.Current.GetInstance<IContentRepository>(); var loader = ServiceLocator.Current.GetInstance<IContentLoader>();

3. Core operations: loading content

Loading content is the most frequent operation. When you know the type, use Get<T>. When you don't, load as IContent and inspect or cast safely.

3.1 Loading a single content item by type

C#
using EPiServer.Core; using EPiServer.Web; ContentReference pageLink = ContentReference.StartPage; // Typical: use IContentLoader for reads var page = _contentLoader.Get<TextPage>(pageLink);

Handling unknown types without exceptions

Select a pattern to expand and see the code.

Pattern matching (recommended)

Load as IContent and use pattern matching. Avoids type-mismatch exceptions and keeps flow explicit.

C#
using EPiServer.Core; IContent content = _contentLoader.Get<IContent>(pageLink); if (content is TextPage textPage) { // Safe access to TextPage properties here }
Try-cast semantics (as keyword)

Use the as keyword if you prefer a null-check approach over pattern matching.

C#
TextPage page = _contentLoader.Get<IContent>(pageLink) as TextPage; if (page == null) { // Not a TextPage (or not loadable) }

3.2 Loading multiple items efficiently

For bulk loads, GetItems is typically more efficient than repeated single calls. It also pairs naturally with language options via LoaderOptions.

C#
using System.Collections.Generic; using EPiServer.Core; using EPiServer.DataAbstraction; using EPiServer.Web; var references = new List<ContentReference> { ContentReference.StartPage, new ContentReference(123), new ContentReference(456) }; var items = _contentLoader.GetItems( references, new LoaderOptions { LanguageLoaderOption.FallbackWithMaster() } );

3.3 Language-specific loading

CMS 12 is built for multilingual sites. Use CultureInfo or language loader options deliberately - otherwise content is loaded based on the current request context, which is sometimes intended and sometimes not.

C#
using System.Globalization; using EPiServer.Globalization; var swedish = _contentLoader.Get<TextPage>(pageLink, CultureInfo.GetCultureInfo("sv")); var current = _contentLoader.Get<TextPage>(pageLink, ContentLanguage.PreferredCulture);

4. Traversing the content tree: listing children

Optimizely content is hierarchical. Listing children is a foundational operation for navigation, listings, and content discovery features.

C#
using System.Globalization; using EPiServer.Core; using EPiServer.Web; ContentReference parentLink = ContentReference.StartPage; var allChildren = _contentLoader.GetChildren<IContent>(parentLink); var childPages = _contentLoader.GetChildren<PageData>(parentLink); var swedishChildren = _contentLoader.GetChildren<IContent>(parentLink, CultureInfo.GetCultureInfo("sv"));

5. Writing content safely: writable clones and save actions

Modifying content programmatically requires one non-negotiable step: create a writable clone. Loaded content instances are treated as immutable snapshots. Update the clone, then save with the correct SaveAction and required AccessLevel.

Important: If you try to modify a loaded content instance without calling CreateWritableClone(), you will either get a runtime exception or end up with changes that silently do not persist.

C#
using System; using EPiServer.Core; using EPiServer.DataAccess; using EPiServer.Security; using EPiServer.Web; ContentReference pageLinkToUpdate = ContentReference.StartPage; // Load via repository when you intend to write TextPage pageToUpdate = _contentRepository.Get<TextPage>(pageLinkToUpdate); // Clone -> modify -> save TextPage writablePage = pageToUpdate.CreateWritableClone() as TextPage; writablePage.MainBody = $"Programmatically updated at {DateTime.Now}."; writablePage.Name = $"Updated Start Page - {DateTime.Now:yyyyMMddHHmmss}"; // SaveAction.Publish publishes immediately; adjust to match your workflow needs _contentRepository.Save(writablePage, SaveAction.Publish, AccessLevel.Publish);

Choosing SaveAction deliberately

Select an action to expand and read when to use it.

SaveAction.Publish

Publishes the content immediately. Use cautiously in automated jobs - publishing without human review may bypass editorial governance. Ensure this is intentional before using in scheduled tasks.

SaveAction.Save

Saves as a draft. Useful when you want the content to go through a review or approval step before going live. Prefer this over Publish in import or migration jobs when editorial sign-off is required.

SaveAction.CheckIn

Checks in changes. Relevant when using check-out/check-in workflows. Ensures the content lock is released after programmatic editing so other editors can continue working on the item.

SaveAction.ForceCurrentVersion

Advanced and rarely needed. Overwrites the current version without creating a new one. Use only when you fully understand the versioning implications - it can silently discard version history in ways that are difficult to recover from.

6. Best practices and operational considerations

These guidelines keep your content operations predictable, reviewable, and easier to maintain - especially in larger PaaS solutions where multiple teams and automated jobs touch content.

  • Use the right interface: IContentLoader for reads, IContentRepository for writes.
  • Always clone before writing: CreateWritableClone() is mandatory for updates.
  • Pick SaveAction intentionally: publishing in a job is very different from saving a draft for review.
  • Enforce permissions: set AccessLevel appropriately to avoid unauthorized modifications.
  • Handle errors explicitly: type mismatches and access denied errors should be visible and actionable in logs.
  • Be language-aware: specify culture or loader options to avoid loading the wrong language version.
  • Prefer batch loading: use GetItems for efficiency and predictable performance.

Pro tip: In code reviews, the fastest signal that a developer has swapped IContentLoader for IContentRepository "just to make it work" is that no Save() call follows. Flag it - it is always worth the conversation.

Conclusion

IContentRepository, complemented by IContentLoader, is a foundational API surface for building dynamic Optimizely CMS 12 solutions.

Keep read and write intent clear, handle languages deliberately, clone before updates, and save with the correct workflow semantics. Following these principles ensures your content operations remain stable and maintainable as the solution grows.