Skip to main content

Outline

At a glance
  • What you’ll learn: How to load content and metadata using the core CMS APIs in CMS 12 (PaaS).
  • Core tools: IContentLoader for reads, IContentRepository mainly for writes (but it can read too).
  • Common scenarios: single item, hierarchy (children/descendants), batch loading, basic property filtering, metadata extraction.
  • Performance reminder: If your “query” starts looking like search + facets + ranking, that’s Optimizely Find territory.


Introduction

Efficiently querying and retrieving content (and the metadata that comes with it) is core to almost every Optimizely CMS 12 build: dynamic navigation, integrations, reporting, page rendering, and “show me the latest X” components.

Optimizely stores content in a hierarchical tree. Editors browse and manage it in the UI; developers fetch and shape it programmatically. This page focuses on the core CMS APIs—primarily IContentLoader and IContentRepository. For advanced search-driven querying (full-text, faceting, relevance ranking), Optimizely Find is the dedicated solution.



Core interfaces for querying

The primary interfaces for programmatic content retrieval in Optimizely CMS 12 are:

  • IContentLoader: Read-only interface for retrieving content. It’s optimized for reads and benefits from built-in caching. For any read operation, IContentLoader should be your first choice.
  • IContentRepository: Primarily used for modifications (create/update/delete), but it also exposes Get methods. If you plan to load-and-modify in one flow, it can be convenient—though IContentLoader is still preferred for pure reads.

Both interfaces are typically obtained through dependency injection.



1. Loading a single content item

The most basic query is retrieving one content item when you know its unique identifier: a ContentReference.

Steps

  1. Identify the ContentReference: e.g., ContentReference.StartPage, or a reference stored in a property.
  2. Load as a specific type: IContentLoader.Get<T> (for type safety and easier property access).
  3. Load generically when needed: IContentLoader.Get<IContent> if the runtime type is unknown.
using EPiServer.Core;
using EPiServer.Web; // ContentReference.StartPage
using System;

public class ContentQueryService
{
    private readonly IContentLoader _contentLoader;

    public ContentQueryService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void LoadSingleContentItem(ContentReference contentLink)
    {
        Console.WriteLine($"Attempting to load content item with ID: {contentLink.ID}");

        // Loading by specific type (e.g., PageData)
        try
        {
            PageData page = _contentLoader.Get<PageData>(contentLink);
            Console.WriteLine($"Loaded as PageData: '{page.PageName}' (Type: {page.GetType().Name})");
        }
        catch (TypeMismatchException)
        {
            Console.WriteLine($"Content at {contentLink.ID} is not a PageData type.");
        }
        catch (ContentNotFoundException)
        {
            Console.WriteLine($"Content at {contentLink.ID} (PageData) not found.");
        }

        // Loading generically as IContent
        try
        {
            IContent genericContent = _contentLoader.Get<IContent>(contentLink);
            Console.WriteLine($"Loaded generically: '{genericContent.Name}' (Actual Type: {genericContent.GetType().Name})");
        }
        catch (ContentNotFoundException)
        {
            Console.WriteLine($"Content at {contentLink.ID} (IContent) not found.");
        }
    }
}

Explanation: Loading by a specific type gives you type-safe access, but can throw TypeMismatchException if the item exists but isn’t assignable to that type. Loading as IContent avoids type mismatch and lets you inspect the runtime type.



2. Querying content by hierarchy

Optimizely’s content tree enables hierarchical querying, which is essential for navigation, sitemaps, and “related content” listings.

Steps

  1. Identify the parent: the starting ContentReference.
  2. Direct children: use GetChildren<T> for one-level-down queries.
  3. All descendants: use GetDescendants<T> to walk the entire subtree.
using EPiServer.Core;
using System;
using System.Collections.Generic;
using System.Linq;

public class ContentQueryService
{
    private readonly IContentLoader _contentLoader;

    public ContentQueryService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void QueryHierarchicalContent(ContentReference parentLink)
    {
        Console.WriteLine($"Querying hierarchical content under ID: {parentLink.ID}");

        // Direct children (pages)
        IEnumerable<PageData> childrenPages = _contentLoader.GetChildren<PageData>(parentLink);

        if (childrenPages.Any())
        {
            foreach (var page in childrenPages)
            {
                Console.WriteLine($"- Child Page: '{page.PageName}' (ID: {page.ContentLink.ID})");
            }
        }
        else
        {
            Console.WriteLine("No direct child pages found.");
        }

        // All descendants (all content types)
        IEnumerable<IContent> allDescendants = _contentLoader.GetDescendants<IContent>(parentLink);

        if (allDescendants.Any())
        {
            foreach (var content in allDescendants)
            {
                Console.WriteLine($"- Descendant: '{content.Name}' (ID: {content.ContentLink.ID}, Type: {content.GetType().Name})");
            }
        }
        else
        {
            Console.WriteLine("No descendants found.");
        }
    }
}

Explanation: GetChildren<T> fits navigation menus. GetDescendants<T> is more “sitemap-style,” and can get expensive if the subtree is large—treat it as a power tool, not a toothbrush.



3. Loading multiple content items by references

If you already have a list of ContentReference values, use GetItems() to load them efficiently instead of calling Get<T> in a loop.

Steps

  1. Collect references: IEnumerable<ContentReference>.
  2. Use GetItems(): optionally supply LoaderOptions for language behavior.
using EPiServer.Core;
using EPiServer.DataAbstraction; // LoaderOptions, LanguageLoaderOption
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

public class ContentQueryService
{
    private readonly IContentLoader _contentLoader;

    public ContentQueryService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void LoadMultipleContentItems(IEnumerable<ContentReference> contentLinks, CultureInfo language)
    {
        Console.WriteLine($"Loading multiple content items for language: {language.Name}");

        var loaderOptions = new LoaderOptions
        {
            LanguageLoaderOption.SpecificCulture(language),
            LanguageLoaderOption.FallbackWithMaster()
        };

        IEnumerable<IContent> items = _contentLoader.GetItems(contentLinks, loaderOptions);

        if (items.Any())
        {
            foreach (var item in items)
            {
                Console.WriteLine($"- Item: '{item.Name}' (ID: {item.ContentLink.ID}, Type: {item.GetType().Name})");
            }
        }
        else
        {
            Console.WriteLine("No content items found for the given references.");
        }
    }
}

Explanation: GetItems() helps you avoid N+1 loading patterns. The LoaderOptions configuration makes language behavior explicit and helps you retrieve the “best available” variant for a given culture.



4. Basic filtering by type and properties

For basic filtering, you can load a set of content and use LINQ in memory. This is fine for small-to-medium collections. For large datasets, complex filtering, full-text search, and faceting, use Optimizely Find.

Steps

  1. Retrieve a collection: children or descendants.
  2. Filter with LINQ: Where, OrderBy, Select, OfType.
  3. Read properties: standard or custom fields.
using EPiServer.Core;
using System;
using System.Collections.Generic;
using System.Linq;

// Assume StandardPage has a custom property 'IsFeatured'
// public class StandardPage : PageData { public virtual string MainBody { get; set; } public virtual bool IsFeatured { get; set; } }

public class ContentQueryService
{
    private readonly IContentLoader _contentLoader;

    public ContentQueryService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void QueryContentByProperties(ContentReference rootLink)
    {
        Console.WriteLine($"Querying content by properties under ID: {rootLink.ID}");

        // Beware: this loads a lot if the subtree is large.
        IEnumerable<StandardPage> allStandardPages = _contentLoader.GetDescendants<StandardPage>(rootLink);

        var featuredPages = allStandardPages
            .Where(page => page.IsFeatured && page.PageName.Contains("Product"))
            .OrderBy(page => page.PageName)
            .ToList();

        if (featuredPages.Any())
        {
            Console.WriteLine("Featured Product Pages:");
            foreach (var page in featuredPages)
            {
                var snippet = page.MainBody == null
                    ? ""
                    : page.MainBody.Substring(0, Math.Min(page.MainBody.Length, 50));

                Console.WriteLine($"- '{page.PageName}' (ID: {page.ContentLink.ID}) | MainBody snippet: {snippet}...");
            }
        }
        else
        {
            Console.WriteLine("No featured product pages found.");
        }
    }
}

Explanation: This is a practical pattern for basic filtering, but it’s still in-memory filtering after loading content. If the subtree is large, shift the problem to a search/index solution instead of brute-forcing it in application code.



5. Accessing content metadata

Every content item includes metadata that helps you understand lifecycle, relationships, scheduling, and version status. This is especially useful for reporting, audits, and operational tooling.

Common metadata fields

  • ContentLink: the unique identifier.
  • Name: the display name (for pages, this typically maps to PageName).
  • Created: when the content was created.
  • Changed: when it was last modified.
  • StartPublish: scheduled publish time (if set).
  • StopPublish: scheduled unpublish time (if set).
  • Status: from IVersionable (e.g., Published, CheckedOut, AwaitingApproval).
  • ParentLink: reference to the parent content item.
  • ContentTypeID: content type definition ID.
using EPiServer.Core;
using EPiServer.DataAbstraction; // IVersionable
using System;

public class ContentQueryService
{
    private readonly IContentLoader _contentLoader;

    public ContentQueryService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void AccessContentMetadata(ContentReference contentLink)
    {
        Console.WriteLine($"Accessing metadata for content item ID: {contentLink.ID}");

        try
        {
            IContent content = _contentLoader.Get<IContent>(contentLink);

            Console.WriteLine($"Name: {content.Name}");
            Console.WriteLine($"ContentLink ID: {content.ContentLink.ID}");
            Console.WriteLine($"ParentLink ID: {content.ParentLink?.ID}");
            Console.WriteLine($"Content Type ID: {content.ContentTypeID}");
            Console.WriteLine($"Created: {content.Created}");
            Console.WriteLine($"Last Changed: {content.Changed}");
            Console.WriteLine($"Start Publish: {content.StartPublish?.ToString() ?? "Not set"}");
            Console.WriteLine($"Stop Publish: {content.StopPublish?.ToString() ?? "Not set"}");

            if (content is IVersionable versionable)
            {
                Console.WriteLine($"Version Status: {versionable.Status}");
            }
        }
        catch (ContentNotFoundException)
        {
            Console.WriteLine($"Content at {contentLink.ID} not found.");
        }
    }
}

Explanation: Metadata is available off IContent. For version status, cast to IVersionable. This gives you a quick operational snapshot without needing to inspect the UI.



6. Best practices for content querying

  • Prefer IContentLoader for reads: optimized, cached, and intent-revealing.
  • Avoid N+1: use GetItems() for batches instead of repeated Get<T> calls.
  • Use hierarchy APIs thoughtfully: GetChildren is safer; GetDescendants can explode on large trees.
  • Be explicit about language: use CultureInfo + LoaderOptions (including fallback behavior).
  • Handle expected exceptions: ContentNotFoundException and TypeMismatchException are normal in real systems.
  • Use Optimizely Find for advanced search: don’t replicate search engines with LINQ over large collections.
  • Prefer strong types where possible: typed loading simplifies property access and reduces runtime casting.


Conclusion

Programmatic content and metadata querying is a cornerstone of Optimizely CMS 12 development. When you master IContentLoader and hierarchical retrieval patterns, you can build efficient navigation, listings, integrations, and operational tooling.

The key is choosing the right approach for the job: core CMS APIs for direct access, Optimizely Find for search-grade querying, and explicit language handling so multilingual behavior stays predictable.