Optimizely CMS 13 hiện đang ở giai đoạn developer preview. Phiên bản này giới thiệu kiến trúc composable và headless-first — kèm theo đó là một số breaking changes yêu cầu migration code thực tế. Bài này hướng dẫn từng bước để nâng cấp backend Ginbok CMS (đang chạy CMS 12 + .NET 8) lên CMS 13.
⚠️ Lưu ý quan trọng: CMS 13 vẫn đang ở preview. Không áp dụng các bước này lên production. Bài viết chỉ phục vụ mục đích học tập và thử nghiệm local. Hướng dẫn chính thức từ Optimizely sẽ có thể khác.
CMS 13 Thay Đổi Gì?
Trước khi đụng vào code, cần hiểu tại sao phải thay đổi. CMS 13 có nhiều thay đổi kiến trúc lớn:
- .NET 10 — target framework tối thiểu tăng từ .NET 6/8 lên .NET 10.
- Application Model thay thế SiteDefinition — cách cấu hình hostname và start page được thiết kế lại hoàn toàn.
- Generic content APIs —
PageReferencevàPageLinkbị obsolete; tất cả chuyển sang dùngContentReferencevàContentLink. - Newtonsoft.Json bị loại bỏ — CMS 13 bỏ hoàn toàn dependency vào Newtonsoft, chuyển sang
System.Text.Json. - Castle.Windsor bị loại bỏ — IoC container cũ không còn; ASP.NET Core DI thuần túy tiếp quản hoàn toàn.
- Plugin system bị xoá hoàn toàn — hệ thống attribute
[EPiServerPlugIn]cũ bị remove. Scheduled jobs có yêu cầu mới. - Dynamic Properties bị xoá — tính năng Dynamic Properties không còn tồn tại trong CMS 13.
- Mirroring bị xoá — tính năng content mirroring bị remove.
Bước 1: Cập Nhật Target Framework
Mở file .csproj của project backend (trong trường hợp Ginbok là project api.ginbok.com) và đổi target framework:
<!-- ❌ Trước -->
<TargetFramework>net8.0</TargetFramework>
<!-- ✅ Sau -->
<TargetFramework>net10.0</TargetFramework>
Nếu bạn có file global.json pin SDK version, cũng cần cập nhật theo.
Bước 2: Cập Nhật NuGet Packages
Cập nhật tất cả packages EPiServer.* lên version 13.0.0-preview2. Đồng thời thêm một package mới mà CMS 13 tách riêng:
<PackageReference Include="EPiServer.CMS" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.CMS.UI" Version="13.0.0-preview2" />
<!-- ✅ Mới: identity management giờ là package riêng -->
<PackageReference Include="EPiServer.CMS.UI.AspNetIdentity" Version="13.0.0-preview2" />
Chạy dotnet restore và đọc kỹ build output — các warning chính là danh sách việc cần làm tiếp theo.
Bước 3: Migrate Newtonsoft.Json Sang System.Text.Json
CMS 13 loại bỏ hoàn toàn Newtonsoft.Json. Nếu project của bạn dùng Newtonsoft ở bất kỳ đâu — custom converters, serialization helpers, hay JSON property attributes — bạn cần migrate sang System.Text.Json.
Các thay đổi phổ biến:
// ❌ Trước (Newtonsoft)
using Newtonsoft.Json;
[JsonProperty("my_field")]
public string MyField { get; set; }
var json = JsonConvert.SerializeObject(obj);
var result = JsonConvert.DeserializeObject<MyClass>(json);
// ✅ Sau (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);
Lưu ý: System.Text.Json strict hơn mặc định — không hỗ trợ reference loop, non-public setter, hay một số edge case mà Newtonsoft xử lý ngầm. Cần test kỹ các phần dùng serialization nhiều.
Bước 4: Thay Thế Các Page API Cũ
CMS 13 thống nhất các content type. PageReference và PageLink bị obsolete — cần cập nhật toàn bộ:
// ❌ Trước
PageReference startRef = SiteDefinition.Current.StartPage;
ContentReference rootRef = currentPage.PageLink;
// ✅ Sau
ContentReference startRef = ContentReference.StartPage;
ContentReference rootRef = currentPage.ContentLink;
Dùng tính năng "Find All References" trong IDE để tìm tất cả chỗ cần sửa — đừng chờ lỗi runtime mới phát hiện.
Bước 5: Thay SiteDefinition Bằng IApplicationResolver
Đây là thay đổi lớn nhất về mặt khái niệm. SiteDefinition.Current không còn nữa. CMS 13 giới thiệu Application Model — mỗi site là một "Application" với type (In-Process hoặc Headless), start page, và danh sách hostname.
Trong các controller, inject IApplicationResolver thay thế:
// ❌ Trước
public class StartPageController : PageControllerBase<StartPage>
{
public IActionResult Index(StartPage currentPage)
{
var site = SiteDefinition.Current;
// ...
}
}
// ✅ Sau
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;
// dùng website.RoutingEntryPoint thay cho SiteDefinition.Current.StartPage
}
}
Với SiteDefinition.Current.RootPage, chỉ cần thay bằng ContentReference.RootPage — vẫn hoạt động bình thường.
Bước 6: Hiện Đại Hoá Dependency Injection
Nếu bạn đang dùng context.Locate.Advanced.GetInstance<T>() (thường gặp trong initialization modules hoặc service locator cũ), hãy chuyển sang constructor injection. Castle.Windsor cũng bị xoá hoàn toàn — mọi registration code kiểu Windsor cần được thay bằng ASP.NET Core DI:
// ❌ Trước
var repo = context.Locate.Advanced.GetInstance<IContentRepository>();
// ✅ Sau — inject IServiceProvider rồi dùng:
var repo = serviceProvider.GetRequiredService<IContentRepository>();
Bước 7: Thay Thế Legacy Plugin System
Hệ thống plugin dựa trên attribute [EPiServerPlugIn] bị xoá hoàn toàn trong CMS 13. Nếu bạn có scheduled jobs hoặc plugin dùng pattern cũ, cần cập nhật lại.
Với scheduled jobs, cách mới yêu cầu implement và đăng ký qua DI:
// ❌ Trước — legacy plugin attribute
[EPiServerPlugIn]
[ScheduledPlugIn(DisplayName = "My Job")]
public class MyScheduledJob : JobBase
{
public override string Execute() { ... }
}
// ✅ Sau — đăng ký như typed scheduled job
[ScheduledPlugIn(DisplayName = "My Job", GUID = "your-guid-here")]
public class MyScheduledJob : ScheduledJobBase
{
public override string Execute() { ... }
}
// Đăng ký trong Program.cs:
services.AddTransient<MyScheduledJob>();
Bước 8: Cập Nhật Cấu Hình Startup.cs
Hai bổ sung bắt buộc trong Startup.cs / Program.cs:
// 1. Cho phép tự động cập nhật DB compatibility level
services.Configure<DataAccessOptions>(options =>
{
options.UpdateDatabaseCompatibilityLevel = true;
});
// 2. Bắt buộc trong preview — fix lỗi menu không render được
services.AddVisitorGroups();
Thêm Content Graph credentials vào appsettings.json. Trong preview, Content Graph bật mặc định và không thể tắt — bạn phải cung cấp key hợp lệ:
"Optimizely": {
"ContentGraph": {
"GatewayAddress": "https://staging.cg.optimizely.com",
"AllowSendingLog": "true",
"SingleKey": "YOUR_SINGLE_KEY",
"AppKey": "YOUR_APP_KEY",
"Secret": "YOUR_SECRET"
}
}
Bước 9: Tạo Lại Application Trong CMS Admin
Sau khi upgrade xong, chạy app và bạn sẽ thấy 404. Đây là bình thường — database vẫn còn cấu hình SiteDefinition cũ. Sửa qua Admin UI:
- Vào
/Optimizely/CMS→ Settings → Applications - Xoá application Headless mặc định được tạo tự động
- Click Create New Application:
- Type: In Process
- Start page: chọn start page hiện tại của Ginbok
- Edit application vừa tạo → Hosts → thêm
localhost:5000làm default
Site của bạn sẽ hiển thị bình thường trở lại.
Frontend Next.js Thì Sao?
Tin tốt: frontend Next.js của Ginbok giao tiếp với CMS qua Content Delivery API — API này vẫn ổn định qua CMS 12 → 13. Các component, API route, và logic fetch content không cần thay đổi cho bước upgrade ban đầu. Ảnh hưởng lớn hơn sẽ đến sau nếu bạn muốn tận dụng các tính năng headless native của CMS 13.
Cảnh Báo Tương Thích Third-Party Packages
Trước khi upgrade, cần kiểm tra khả năng tương thích của các package bên thứ ba. Một số incompatibility đã biết trong preview hiện tại:
- Optimizely Find — chưa tương thích với CMS 13. Nếu site dùng Find để search, cần chờ phiên bản Find được cập nhật.
- Optimizely Forms — có incompatibility đã biết trong preview. Kiểm tra release notes chính thức trước khi upgrade.
- Geta packages (ví dụ: Geta.NotFoundHandler, Geta.Categories) — tương thích tùy package; cần xác nhận từng cái một.
- Extension/community packages khác — bất kỳ package nào phụ thuộc vào plugin system, Castle.Windsor, Newtonsoft.Json, hoặc SiteDefinition APIs đều cần được maintainer cập nhật.
Kiểm tra danh sách tương thích chính thức trước khi bắt đầu upgrade.
Checklist Tổng Hợp
- ✅ Nâng target framework lên
net10.0 - ✅ Cập nhật tất cả EPiServer packages lên
13.0.0-preview2 - ✅ Thêm package
EPiServer.CMS.UI.AspNetIdentity - ✅ Migrate từ
Newtonsoft.JsonsangSystem.Text.Json - ✅ Bỏ Castle.Windsor; dùng ASP.NET Core DI hoàn toàn
- ✅ Thay
PageReference→ContentReferencetoàn bộ - ✅ Thay
PageLink→ContentLinktoàn bộ - ✅ Refactor controller dùng
IApplicationResolver - ✅ Thay service locator bằng constructor injection
- ✅ Thay legacy
[EPiServerPlugIn]bằng scheduled job pattern mới - ✅ Xoá mọi usage của Dynamic Properties và Mirroring
- ✅ Thêm
UpdateDatabaseCompatibilityLevelvàAddVisitorGroups() - ✅ Thêm Content Graph credentials vào
appsettings.json - ✅ Kiểm tra tương thích third-party packages (Find, Forms, Geta)
- ✅ Tạo lại Application qua CMS Admin UI
Nguồn tham khảo: Robert Svallin — From 12 to 13: A Developer's Guide · Optimizely Official Breaking Changes Docs