Engineering Notes

Upgrading Ginbok CMS from Optimizely 12 to 13: A Step-by-Step Developer Guide

By Ginbok7 min read

Optimizely CMS 13 is currently available as a developer preview. It introduces a more composable, headless-first architecture — and with it, several breaking changes that require hands-on code migration. This guide walks through what upgrading the Ginbok CMS backend (currently on CMS 12 + .NET 8) would look like, step by step.

⚠️ Important: CMS 13 is still in preview. Do not apply these steps to a live production site. This guide is for learning and local experimentation only. The official release guidance from Optimizely will differ.

What Changes in CMS 13?

Before touching any code, it helps to understand why things need to change. CMS 13 makes several major architectural shifts:

Step 1: Update the Target Framework

Open your .csproj file (in Ginbok's case, the api.ginbok.com backend project) and change the target framework:

<!-- ❌ Before -->
<TargetFramework>net8.0</TargetFramework>

<!-- ✅ After -->
<TargetFramework>net10.0</TargetFramework>

You'll also need to update your SDK version in global.json if you have one pinned.

Step 2: Update NuGet Packages

Update all EPiServer.* packages to version 13.0.0-preview2. Also add a new package that CMS 13 now requires separately:

<PackageReference Include="EPiServer.CMS" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.CMS.UI" Version="13.0.0-preview2" />

<!-- ✅ New: identity management is now a separate package -->
<PackageReference Include="EPiServer.CMS.UI.AspNetIdentity" Version="13.0.0-preview2" />

Run dotnet restore and check the build output for warnings — those warnings are your migration checklist.

Step 3: Migrate from Newtonsoft.Json to System.Text.Json

CMS 13 drops the Newtonsoft.Json dependency entirely. If your project uses Newtonsoft anywhere — custom converters, serialization helpers, or JSON property attributes — you need to migrate to System.Text.Json.

Common changes:

// ❌ Before (Newtonsoft)
using Newtonsoft.Json;

[JsonProperty("my_field")]
public string MyField { get; set; }

var json = JsonConvert.SerializeObject(obj);
var result = JsonConvert.DeserializeObject<MyClass>(json);

// ✅ After (System.Text.Json)
using System.Text.Json.Serialization;

[JsonPropertyName("my_field")]
public string MyField { get; set; }

var json = JsonSerializer.Serialize(obj);
var result = JsonSerializer.Deserialize<MyClass>(json);

Note: System.Text.Json is stricter by default — it does not support reference loops, non-public setters, or some edge-case conversions that Newtonsoft handled silently. Test serialization-heavy areas carefully.

Step 4: Replace Obsolete Page APIs

CMS 13 unifies content types. PageReference and PageLink are now obsolete — every place you used them needs to be updated:

// ❌ Before
PageReference startRef = SiteDefinition.Current.StartPage;
ContentReference rootRef = currentPage.PageLink;

// ✅ After
ContentReference startRef = ContentReference.StartPage;
ContentReference rootRef = currentPage.ContentLink;

Use your IDE's "Find All References" to locate every occurrence across the solution — don't rely on runtime errors to catch these.

Step 5: Replace SiteDefinition with IApplicationResolver

This is the biggest conceptual change. SiteDefinition.Current is gone. CMS 13 introduces an Application Model — each site is an "Application" with a type (In-Process or Headless), a start page, and hostnames.

In your controllers, inject IApplicationResolver instead:

// ❌ Before
public class StartPageController : PageControllerBase<StartPage>
{
    public IActionResult Index(StartPage currentPage)
    {
        var site = SiteDefinition.Current;
        // ...
    }
}

// ✅ After
public class StartPageController : PageControllerBase<StartPage>
{
    private readonly IApplicationResolver _applicationResolver;

    public StartPageController(IApplicationResolver applicationResolver)
    {
        _applicationResolver = applicationResolver;
    }

    public async Task<IActionResult> Index(StartPage currentPage, CancellationToken cancellationToken)
    {
        var application = await _applicationResolver.GetByContextAsync(cancellationToken);
        var website = application as Website;
        // use website.RoutingEntryPoint instead of SiteDefinition.Current.StartPage
    }
}

For SiteDefinition.Current.RootPage, simply replace it with ContentReference.RootPage — that still works unchanged.

Step 6: Modernize Dependency Injection

If you're using context.Locate.Advanced.GetInstance<T>() anywhere (common in initialization modules or old service locator patterns), replace it with proper constructor injection. Castle.Windsor is also completely removed, so any Windsor-specific registration code must be replaced with ASP.NET Core DI equivalents:

// ❌ Before
var repo = context.Locate.Advanced.GetInstance<IContentRepository>();

// ✅ After — inject IServiceProvider and use:
var repo = serviceProvider.GetRequiredService<IContentRepository>();

Step 7: Replace the Legacy Plugin System

The [EPiServerPlugIn] attribute-based plugin system is completely removed in CMS 13. If you have any scheduled jobs or plugins using this pattern, they must be updated.

For scheduled jobs, the new approach requires implementing IScheduledJob and registering via DI:

// ❌ Before — legacy plugin attribute
[EPiServerPlugIn]
[ScheduledPlugIn(DisplayName = "My Job")]
public class MyScheduledJob : JobBase
{
    public override string Execute() { ... }
}

// ✅ After — register as a typed scheduled job
[ScheduledPlugIn(DisplayName = "My Job", GUID = "your-guid-here")]
public class MyScheduledJob : ScheduledJobBase
{
    public override string Execute() { ... }
}
// Register in Program.cs:
services.AddTransient<MyScheduledJob>();

Step 8: Update Startup.cs Configuration

Two required additions in Startup.cs / Program.cs:

// 1. Allow the DB compatibility level to auto-update
services.Configure<DataAccessOptions>(options =>
{
    options.UpdateDatabaseCompatibilityLevel = true;
});

// 2. Required in preview — fixes broken menu rendering
services.AddVisitorGroups();

Also add Content Graph credentials to appsettings.json. In the preview, Content Graph is enabled by default and cannot be disabled — you must supply valid keys:

"Optimizely": {
  "ContentGraph": {
    "GatewayAddress": "https://staging.cg.optimizely.com",
    "AllowSendingLog": "true",
    "SingleKey": "YOUR_SINGLE_KEY",
    "AppKey": "YOUR_APP_KEY",
    "Secret": "YOUR_SECRET"
  }
}

Step 9: Rebuild the Application in CMS Admin

After upgrading, run the app and you'll likely see a 404. This is expected — the database still has the old SiteDefinition. Fix it through the admin UI:

  1. Go to /Optimizely/CMSSettingsApplications
  2. Delete the default Headless application that was auto-created
  3. Click Create New Application:
    • Type: In Process
    • Start page: your existing Ginbok start page
  4. Edit the new app → Hosts → add localhost:5000 as default

Your site should now render correctly on the frontend.

What About the Next.js Frontend?

The good news: Ginbok's Next.js frontend communicates with the CMS via the Content Delivery API — which remains stable across CMS 12 → 13. Your frontend components, API routes, and content fetching logic should not need changes for the initial upgrade. The bigger impact would come if you later adopt CMS 13's native headless features.

Third-Party Package Compatibility Warning

Before upgrading, check compatibility for any third-party packages your project uses. Several known incompatibilities exist in the current preview:

Check the official compatibility list before starting your upgrade.

Summary Checklist

Sources: Robert Svallin — From 12 to 13: A Developer's Guide · Optimizely Official Breaking Changes Docs

#optimizely#cms13#dotnet#upgrade#episerver#cms12
← Back to Articles