Exam Area: Content Area 5 – Content Models (25%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970329 | /4970358
Published: Feb 17 / Feb 25, 2026
- Read vs. Write: Distinguish between
IContentLoader(caching, read-only) andIContentRepository(full CRUD).- CreateWritableClone(): Content instances are immutable snapshots — must clone before modifying.
- Bulk Loading: Use
GetItems()instead of aGet()loop for optimal caching.- Taxonomy Control:
CategoryRepositorymanages hierarchical categories; writable clone needed to update.- Media:
MediaData+ Blob API to separate binary storage from the SQL database.
Key points from Academy (4970329 / 4970358):
ISynchronizedObjectInstanceCache) — automatic caching participationCultureInfo and fallback mechanismsusing EPiServer;
using EPiServer.Core;
public class ContentService
{
private readonly IContentRepository _contentRepository;
private readonly IContentLoader _contentLoader;
// ✅ PRINCIPLE: Use IContentLoader for reads; IContentRepository for writes
public ContentService(IContentRepository contentRepository, IContentLoader contentLoader)
{
_contentRepository = contentRepository;
_contentLoader = contentLoader;
}
// Load single content by reference
public ArticlePage GetArticle(ContentReference link)
{
return _contentLoader.Get<ArticlePage>(link);
}
// Load children
public IEnumerable<ArticlePage> GetChildren(ContentReference parentLink)
{
return _contentLoader.GetChildren<ArticlePage>(parentLink);
}
// GetItems – BULK LOADING (more efficient than loop Get())
public IEnumerable<IContent> BulkLoad(IList<ContentReference> references)
{
return _contentLoader.GetItems(
references,
new LoaderOptions { LanguageLoaderOption.FallbackWithMaster() }
);
}
// Get ancestors (for breadcrumbs)
public IEnumerable<IContent> GetAncestors(ContentReference contentLink)
{
return _contentLoader.GetAncestors(contentLink);
}
// Language-specific loading
public ArticlePage GetInLanguage(ContentReference link, string lang)
{
return _contentLoader.Get<ArticlePage>(link, new CultureInfo(lang));
}
// TryGet - no exception if not found
public bool TryGetArticle(ContentReference link, out ArticlePage article)
{
return _contentLoader.TryGet(link, out article);
}
}
// FallbackWithMaster: return master language if requested lang not found
_contentLoader.Get<PageData>(link, new LoaderOptions { LanguageLoaderOption.FallbackWithMaster() });
// MasterLanguage: always retrieve master version
_contentLoader.Get<PageData>(link, new LoaderOptions { LanguageLoaderOption.MasterLanguage() });
Overhead Consideration: Write operations involve database transactions, search indexing triggers, and cache invalidation. Use IContentLoader for reads to avoid unnecessary transaction overhead.
public void ProcessUpdate(ContentReference link)
{
// READ: Using optimized loader
var product = _contentLoader.Get<ProductPage>(link);
// WRITE: Writable clone required for modifications
var writableProduct = product.CreateWritableClone() as ProductPage;
writableProduct.Price = 99.99m;
_contentRepository.Save(writableProduct, SaveAction.Publish, AccessLevel.NoAccess);
}
public ContentReference CreateArticle(ContentReference parentLink)
{
var article = _contentRepository.GetDefault<ArticlePage>(parentLink);
article.Name = "New Article";
article.Heading = "My Heading";
return _contentRepository.Save(article, SaveAction.Publish, AccessLevel.NoAccess);
}
// ⚠️ CRITICAL: Content instances from the repository are immutable snapshots
// Must call CreateWritableClone() before modifying – MANDATORY
// ❌ WRONG - will throw InvalidOperationException
var page = _contentLoader.Get<ArticlePage>(link);
page.Heading = "New Heading"; // ❌ Exception!
// ✅ CORRECT
var page = _contentRepository.Get<ArticlePage>(link); // Load via repository for write intent
var writablePage = page.CreateWritableClone() as ArticlePage;
writablePage.Name = `Updated Start Page - ${DateTime.Now:yyyyMMddHHmmss}`;
writablePage.MainBody = `Programmatically updated at ${DateTime.Now}.`;
// SaveAction.Publish publishes immediately; adjust to match workflow needs
_contentRepository.Save(writablePage, SaveAction.Publish, AccessLevel.Publish);
public void CreateCustomCategory(string categoryName)
{
var repository = ServiceLocator.Current.GetInstance<CategoryRepository>();
var parent = repository.Get("IndustryRelated");
// Create new instance with parent reference
var newCat = new Category(parent, categoryName)
{
Selectable = true,
Available = true
};
repository.Save(newCat);
}
Notes:
.CreateWritableClone() to modifySaveAction.Save // Save as draft
SaveAction.Publish // Publish immediately
SaveAction.CheckIn // Check in
SaveAction.CheckOut // Checkout for editing
SaveAction.RequestApproval // Request approval
SaveAction.ForceCurrentVersion // Force version update
SaveAction.SchedulePublish // Schedule for future publish
public class VersionService
{
private readonly IContentVersionRepository _versionRepository;
public VersionService(IContentVersionRepository versionRepository)
{
_versionRepository = versionRepository;
}
// Get all versions
public IEnumerable<ContentVersion> GetVersions(ContentReference link)
{
return _versionRepository.List(link);
}
// Get published version
public ContentVersion GetPublished(ContentReference link)
{
return _versionRepository.Load(link, LanguageSelector.AutoDetect());
}
// Delete specific version
public void DeleteVersion(ContentReference versionLink)
{
_versionRepository.Delete(versionLink);
}
// Set version as published
public void SetPublished(ContentReference versionLink)
{
_versionRepository.SetCommonDraft(versionLink);
}
}
// Load published version (default behavior)
var published = _contentLoader.Get<ArticlePage>(link);
// Load specific version
var draft = _contentLoader.Get<ArticlePage>(
new ContentReference(link.ID, versionId));
// Load latest draft (workID = 0 gets published)
var withVersionParams = new ContentReference(link.ID, 0, link.ProviderName);
// Common ContentReference constants
ContentReference.RootPage // Root page (ID=1)
ContentReference.StartPage // Start page
ContentReference.WasteBasket // Recycle bin
ContentReference.GlobalBlockFolder // Shared blocks folder
ContentReference.SiteBlockFolder // Site blocks folder
// Empty check
ContentReference.IsNullOrEmpty(link)
// Create new reference
var link = new ContentReference(pageId: 123);
var withVersion = new ContentReference(pageId: 123, versionId: 456);
// AutoDetect based on current request language
LanguageSelector.AutoDetect()
LanguageSelector.AutoDetect(fallbackToMaster: true)
// Specific language
new LanguageSelector("en")
new LanguageSelector("sv")
// Master language
LanguageSelector.MasterLanguage()
// Load in specific language
var swedishPage = _contentLoader.Get<ArticlePage>(link, new LanguageSelector("sv"));
IContentLoader: Always default to the loader for frontend componentsGetItems() when retrieving a collection of references instead of iterating with Get() to enable bulk cachingAccessLevel in repository operationsIContentLoader and IContentRepository? (Loader: read-only, cached; Repository: full CRUD with transaction overhead)CreateWritableClone()? (Content objects are immutable snapshots when read from cache)SaveAction.Publish vs SaveAction.Save? (Publish: immediately public; Save: draft visible to editors only)CultureInfo or LanguageLoaderOption.FallbackWithMaster())GetItems() more efficient than a Get() loop? (Batch caching optimization – fewer database roundtrips)CategoryRepository require for updating a category? (CreateWritableClone() like IContent)