Skip to main content

Outline

At a glance:
  • Use this for reads: IContentLoader (fast and cache-friendly).
  • Avoid: N+1 loading patterns, LINQ over the whole tree, and repeated loads in the same request.
  • Language matters: use LoaderOptions and LanguageLoaderOption intentionally.
  • Reporting limit: CMS repositories can audit content and metadata, not analytics or aggregates.

1. Introduction

Efficient content retrieval is one of those "invisible" features users immediately notice when it goes wrong. Slow navigation menus, laggy landing pages, and timeouts during heavy traffic often trace back to how content is queried.

Optimizely CMS 12 provides solid core APIs for retrieving content, but you still have to use them with intent. These APIs are optimized for content access and content management workflows - not for analytics-style reporting. Knowing that boundary helps you pick the right tool and prevents your code from turning into a DIY data warehouse.

2. Mastering read-only querying with IContentLoader

For read-only retrieval, IContentLoader is the first choice. It is designed for fast reads and benefits from Optimizely's caching behavior, so you avoid unnecessary database hits.

The role of IContentLoader

Inject IContentLoader and use it anywhere you need to read content without modifying it. Compared to "load then maybe modify," this keeps intent clear and reduces accidental write-path complexity.

Basic retrieval

  • Get<T>(ContentReference) - type-safe retrieval; throws TypeMismatchException if the type doesn't match.
  • Get<IContent>(ContentReference) - generic retrieval when the type isn't known.
C#
using EPiServer.Core; using System; public class ReadOnlyQueryService { private readonly IContentLoader _contentLoader; public ReadOnlyQueryService(IContentLoader contentLoader) { _contentLoader = contentLoader; } public void GetBasicContent(ContentReference contentLink) { try { PageData page = _contentLoader.Get<PageData>(contentLink); Console.WriteLine($"Loaded Page: {page.PageName}"); IContent content = _contentLoader.Get<IContent>(contentLink); Console.WriteLine($"Loaded Generic Content: {content.Name}"); } catch (ContentNotFoundException) { Console.WriteLine($"Content with ID {contentLink.ID} not found."); } catch (TypeMismatchException) { Console.WriteLine($"Content with ID {contentLink.ID} is not of the expected type."); } } }

Hierarchical retrieval

  • GetChildren<T>(parent) - direct children only (great for menus and single-level lists).
  • GetDescendants<T>(parent) - recursive traversal; excellent for admin tools, but risky on front-end hot paths.
C#
using EPiServer.Core; using System.Collections.Generic; using System.Linq; public class ReadOnlyQueryService { private readonly IContentLoader _contentLoader; public ReadOnlyQueryService(IContentLoader contentLoader) { _contentLoader = contentLoader; } public void GetHierarchicalContent(ContentReference parentLink) { IEnumerable<PageData> children = _contentLoader.GetChildren<PageData>(parentLink); Console.WriteLine($"Children of {parentLink.ID}: {children.Count()} pages"); IEnumerable<IContent> descendants = _contentLoader.GetDescendants<IContent>(parentLink); Console.WriteLine($"Descendants of {parentLink.ID}: {descendants.Count()} items"); } }

Batch retrieval

Batch loading prevents the classic N+1 problem (looping through references and calling Get repeatedly). If you already have a list of ContentReference values, prefer GetItems.

C#
using EPiServer.Core; using EPiServer.DataAbstraction; using System.Collections.Generic; using System.Globalization; using System.Linq; public class ReadOnlyQueryService { private readonly IContentLoader _contentLoader; public ReadOnlyQueryService(IContentLoader contentLoader) { _contentLoader = contentLoader; } public void GetBatchContent(IEnumerable<ContentReference> contentLinks, CultureInfo language) { LoaderOptions options = new LoaderOptions { LanguageLoaderOption.SpecificCulture(language), LanguageLoaderOption.FallbackWithMaster() }; IEnumerable<IContent> items = _contentLoader.GetItems(contentLinks, options); Console.WriteLine($"Loaded {items.Count()} items in batch for language {language.Name}"); } }

Language-aware retrieval

On multilingual sites, "correct" content depends on language selection and fallback rules. Prefer being explicit: specify a culture and decide whether you want fallback to master.

Note - language behavior:
  • Using SpecificCulture without fallback can expose "missing translation" gaps (sometimes intended).
  • FallbackWithMaster improves user experience, but can hide editorial completeness issues if you don't audit.

3. Performance considerations for content querying

Most performance issues come from "too many calls" or "too much content." The APIs are fine - your loops are usually the villain.

Caching mechanisms

Optimizely includes caching behavior that IContentLoader benefits from automatically - one key reason it is preferred for reads.

  • Built-in caching: repeated reads can be served from cache instead of the database.
  • Automatic invalidation: publishing or updating content triggers cache invalidation so you don't serve stale versions.
  • Custom caching caution: add extra caching only when you can prove it helps and you understand the invalidation implications.

Optimizing queries

Query optimization patterns

Select a pattern to expand and read the guidance.

Avoid N+1: use GetItems instead of looping Get

Looping through a list of ContentReference values and calling Get on each one is the classic N+1 problem. If you already have the references, use GetItems to load them in a single call. The efficiency difference grows proportionally with the number of items.

Minimize roundtrips per request

Structure retrieval so each request does the fewest necessary content loads. Gather all required references before loading, batch where possible, and avoid re-loading items you've already retrieved within the same request lifecycle.

In-memory filtering: when it is and isn't fine
  • Small sets: loading direct children and applying LINQ filtering is fine and readable.
  • Large sets: "load everything then filter" becomes expensive fast. For large or unbounded sets, move filtering upstream or use a search service.
Prefer specific types over IContent

Querying a concrete type like StandardPage is usually cleaner and more self-documenting than querying IContent and casting. It also makes type mismatches fail loudly rather than silently returning unexpected content.

Lazy loading vs eager loading

When you load a content item, you typically get its properties as part of that load. That is generally helpful because it avoids "property-by-property" query patterns. The trade-off is memory pressure if you load too many large items in one go - so the real fix is still: load fewer items.

Pro tip: The fastest fix for most CMS performance problems is "load less." Before reaching for caching or architectural changes, check whether you can narrow the query - use a more specific type, reduce the depth of tree traversal, or pass an explicit page size to GetChildren.

4. Reporting limitations of core CMS APIs

IContentLoader and IContentRepository are excellent for content retrieval and content management. They are not an analytics engine, and they don't store behavioral metrics like pageviews or conversions.

What core APIs can do

  • Basic listings: content lists by hierarchy or type, plus simple property checks.
  • Metadata extraction: created/changed timestamps, authorship info, publish windows, and version status.
  • Simple audits: "missing meta description," "old tracking ID," "broken references," and similar hygiene checks.

What core APIs cannot do

Important: If the question sounds like "search," "insights," "metrics," or "cross-system reporting," core CMS APIs are not the right tool. These workloads need specialized systems.

  • Aggregations of user behavior: pageviews, sessions, bounce rate, and conversions are not in the CMS database.
  • Historical trends: "month over month performance" requires analytics tooling.
  • Complex analytics: funnels, cohorts, segmentation logic, and experiment results live elsewhere.
  • Non-CMS datasets: commerce orders, CDP profiles, and CRM data need their own APIs or a shared reporting store.
  • Advanced full-text search: ranking, fuzziness, faceting, and relevancy tuning belong to a search product such as Optimizely Find.

When to use specialized tools

  • Optimizely Find: complex search, filtering, faceting, and content discovery.
  • Analytics platforms (GA4, ODP): visitor behavior, performance metrics, and personalization signals.
  • Data warehouse and BI: multi-source reporting across CMS, Commerce, ODP, CRM, and other systems.

Conclusion

Fast CMS sites are usually fast because they are disciplined: read with IContentLoader, batch where possible, avoid N+1, and don't traverse huge trees on hot paths.

For reporting, treat CMS repositories as content tools: great for audits, metadata, and structured listings. When you need advanced search or analytics, reach for the specialized systems built for those workloads.