Monday, 5 October 2026

Migrating from Search & Navigation (Find) to Optimizely Graph in CMS 13

If you are planning an upgrade to Optimizely CMS 13, one of the biggest architectural shifts is that Optimizely Search & Navigation (EPiServer.Find) is completely unsupported. It is not just deprecated; the packages will not run on CMS 13. Every site moving to CMS 13 must replace Find with Optimizely Graph.

The good news is that Optimizely provides a dedicated C# query package for CMS 13 (Optimizely.Graph.Cms.Query) that offers a fluent API structured very similarly to the old Find client. However, there are significant changes under the hood, particularly around package names, configuration keys, dependency injection, and a move from synchronous to asynchronous query execution.

Here is a step-by-step walkthrough to help you migrate your codebase cleanly.

Package Replacements

First, remove all legacy Find and related packages from your project files:

<!-- Remove legacy Find packages -->
<PackageReference Remove="EPiServer.Find" />
<PackageReference Remove="EPiServer.Find.Cms" />
<PackageReference Remove="EPiServer.Find.Framework" />
<PackageReference Remove="Wangkanai.Detection" />

Next, install the Optimizely Graph packages for CMS 13:

dotnet add package Optimizely.Graph.Cms.Query
dotnet add package Optimizely.Graph.AspNetCore

Optimizely.Graph.Cms.Query provides the strongly-typed LINQ-style query builder, while Optimizely.Graph.AspNetCore handles middleware and client lifecycle integration.

Configuration Changes in appsettings.json

Remove the legacy EPiServer:Find section from appsettings.json:

// Remove this block
"EPiServer": {
  "Find": {
    "ServiceUrl": "https://...",
    "DefaultIndex": "your_index_name"
  }
}

Replace it with the new Optimizely:Graph section:

"Optimizely": {
  "Graph": {
    "GatewayAddress": "https://cg.optimizely.com",
    "AppKey": "YOUR_APP_KEY",
    "Secret": "YOUR_SECRET_KEY",
    "SingleKey": "YOUR_SINGLE_KEY"
  }
}

On local environments, populate these keys from your Graph portal. In Optimizely DXP, these environment variables are provisioned automatically at deployment time.

Service Registration and Middleware

In your Startup.cs or Program.cs, swap the service registrations and middleware:

// Remove legacy Find calls:
// services.AddFind();
// services.AddDetection();
// app.UseDetection();

// Add Optimizely Graph:
services.AddContentGraph();
services.AddGraphContentClient();

// Add Graph tracking middleware
app.UseGraphTrackingScripts();

Namespace Updates

Find queries used namespaces scattered across EPiServer.Find.*. In CMS 13, the majority map directly to Optimizely.Graph.Cms.Query:

Legacy Find Namespace Optimizely Graph Replacement
EPiServer.Find Optimizely.Graph.Cms.Query
EPiServer.Find.Cms Optimizely.Graph.Cms.Query
EPiServer.Find.Api Optimizely.Graph.Cms.Query
EPiServer.Find.Framework Optimizely.Graph.Cms.Query

You can remove older references such as EPiServer.Find.Framework.Statistics and EPiServer.Find.Helpers.Text as they are no longer used.

Query Rewriting: Client and Execution Model

The single most impactful code change is how queries execute. Find allowed synchronous calls like .GetContentResult() or .GetResult(). Optimizely Graph is strictly asynchronous and relies on dependency injection rather than static singleton access.

1. Client Dependency Injection

Instead of SearchClient.Instance or injecting IClient, inject IGraphContentClient:

public class SearchPageController : PageController<SearchPage>
{
    private readonly IGraphContentClient _graphClient;

    public SearchPageController(IGraphContentClient graphClient)
    {
        _graphClient = graphClient;
    }

    // ...
}

2. Querying Content with GetAsContentAsync

In Find, fetching full content items typically looked like this:

// Legacy Find (Synchronous)
var results = SearchClient.Instance.Search<ArticlePage>()
    .For(query)
    .InField(x => x.Title)
    .Filter(x => x.Category.Match("News"))
    .Take(10)
    .GetContentResult();

In Optimizely Graph for CMS 13, use QueryContent<T>() and GetAsContentAsync():

// Optimizely Graph (Async)
var results = await _graphClient.QueryContent<ArticlePage>()
    .SearchFor(query)
    .UsingFullText()
    .Where(x => x.Category == "News")
    .Limit(10)
    .GetAsContentAsync();

GetAsContentAsync() returns an IGetAsContentResult<T> that implements IEnumerable<T>, delivering fully resolved CMS content models without requiring extra mapping layers.

3. Dynamic Filtering with BuildFilter

In Find, dynamic search criteria were typically composed by chaining .Filter() calls. In the Graph C# SDK, use BuildFilter<T>() to assemble conditional logic dynamically:

var filterBuilder = _graphClient.BuildFilter<ArticlePage>()
    .And(x => x.Name.FilterStartsWith("Tech"));

if (!string.IsNullOrEmpty(selectedAuthor))
{
    filterBuilder = filterBuilder.And(x => x.Author.Eq(selectedAuthor));
}

var results = await _graphClient.QueryContent<ArticlePage>()
    .Where(filterBuilder)
    .GetAsContentAsync();

4. Faceted Navigation

Facets work differently in Graph. When users filter by an author, you often want the facet counts for all other authors to remain visible so users can switch options easily. The Facet() extension accepts a filters parameter directly for this purpose:

var results = await _graphClient.QueryContent<ArticlePage>()
    .SearchFor(query)
    .UsingFullText()
    .Facet(x => x.Author, filters: [selectedAuthor])
    .GetAsContentAsync();

var authorFacet = results.Facets?.GetFacet(x => x.Author);

Important

  • Do not mix the platform upgrade with the search migration. Upgrade your project to CMS 13 first, verify that it compiles and starts, and then execute the Find to Graph migration on a dedicated Git branch.
  • Audit all synchronous helper methods, extensions, and custom view models. Because Graph requires await for execution, synchronous wrappers (such as .GetAwaiter().GetResult()) can cause thread pool starvation under production loads on ASP.NET Core. Convert calling methods to async up the stack.
  • Remember that CMS 13 introduces a breaking schema change in Graph compared to CMS 12. If you are already testing Graph on CMS 12, provision a separate Graph instance for your CMS 13 deployment slot to avoid corrupting your production index. Read more about this here.
  • Property indexing modes matter for query performance. Mark fields that are only displayed in results (and never searched or filtered) with the [GraphProperty(PropertyIndexingMode.OutputOnly)] attribute to minimize Graph indexing overhead.

Links

October 05, 2026 →

Thursday, 1 October 2026

Why Your CMS 12 and CMS 13 Sites Can't Share a Graph Instance

If you're planning your Optimizely CMS 13 upgrade and you already run Optimizely Graph on your CMS 12 site, there's one detail that catches almost everyone off guard: you cannot point your CMS 13 environment at the same Graph instance your live CMS 12 site is using. Not temporarily, not just for testing. CMS 13 ships a breaking change to the Graph schema, and a CMS 12 site and a CMS 13 site cannot share a Graph instance without migrating the schema first.

This isn't an optional step you can defer. Graph is required to use CMS 13's full capabilities, including Content Manager and Experiences, so for any site already running Graph on CMS 12, migrating it is part of the CMS 13 upgrade itself, not a follow-up task.

Why This Happens

CMS 13 changes what Graph indexes and how it's shaped. Experiences, Sections, and Visual Builder's composition model change the object graph that gets synced from your CMS content tree, so the schema CMS 13 expects Graph to serve is different from the one CMS 12 populates. Point a CMS 13 site at a CMS 12 Graph instance (or vice versa) and queries either fail outright or quietly return the wrong shape of data.

That's why the upgrade guidance is blunt about it: treat the Graph migration as a required part of moving to CMS 13, not a nice-to-have cleanup step you can do later.

Two Ways to Handle the Migration

You have two supported paths. Which one you pick depends mostly on whether your site is live and how much downtime you can tolerate.

Option 1: Provision a New Graph Instance (Recommended, Zero Downtime)

Your live CMS 12 site keeps using its existing Graph instance while your CMS 13 deployment slot uses a brand new one. Graph never goes down for either version.

  1. Log in to the DXP Management Portal at https://paasportal.episerver.net.
  2. Open your organization and project, then go to the API tab and open Optimizely Graph Service Configurations. Click Prepare for CMS 13 to provision a separate Graph instance.
  3. Deploy CMS 13 through a deployment slot. The slot uses the new Graph credentials while production keeps using the old ones.
  4. Validate the slot against the CMS 13 schema without touching your live CMS 12 Graph data.
  5. Swap the slot into production. Production automatically picks up the new CMS 13 Graph credentials.

The old CMS 12 Graph instance isn't deleted immediately. It's retired in two stages so anything still pointing at the old keys has time to surface:

  • Stage 1 - Key rotation (soft delete): runs once all linked front-end environments are confirmed on the new key and a grace period (default seven days) has elapsed since that confirmation — or, if there are no linked front-end environments, seven days after go-live. This disables the old keys, so anything still using them starts failing visibly.
  • Stage 2 - Hard delete: the old Graph instance is removed a further 30 days after Stage 1.

That visible failure in Stage 1 is a feature, not a bug. It's your forcing function for finding every forgotten front end, webhook, or scheduled job that still has the old single key hardcoded somewhere.

Option 2: Smooth Rebuild on Your Existing Instance (Downtime Required)

If you'd rather keep your existing Graph instance and credentials (so external consumers never need to update anything), you can migrate the schema in place instead. This requires taking the site offline.

  1. Put the site behind a static maintenance page.
  2. In CMS, go to Settings > Smooth Rebuild, open Smooth Rebuild Settings, and click Start.
  3. Graph creates a new deployment slot (green) alongside the current one (blue) and starts syncing content into it.
  4. Test the green slot before promoting it. Use the Optimizely Graph playground (GraphiQL) with the New toggle enabled, or add the cg-query-new: true header to your GraphQL requests to target it directly.
  5. Deploy CMS 13 through a deployment slot once the rebuild has finished syncing.
  6. Promote the green slot to live.
cg-query-new: true

Because the CMS 13 schema change is breaking, you can't use Graph's normal zero-downtime read-only or read-write slot modes here. The site has to be fully offline behind the maintenance page for the duration of the rebuild.

Important

  • If you haven't implemented Optimizely Graph on your CMS 12 project at all, none of this applies to you. Follow the standard DXP upgrade path through Integration, Preproduction, and Production.
  • Run whichever path you choose in Integration and Preproduction first. You need to know how long the sync or rebuild actually takes before you size a Production maintenance window around it.
  • Audit anything with a hardcoded Graph single key (Next.js front ends, webhooks, scheduled jobs) before you start. Key rotation will break them loudly, which is useful, but you'd rather know in advance than get paged for it.
  • Local development config (GatewayAddress, AppKey, Secret, SingleKey in appsettings.json or web.config) is separate from what DXP auto-provisions on deployment. Don't carry your CMS 12 Graph configuration into your CMS 13 deployment and assume it'll just work.

Which Option Should You Pick?

Situation Go with
Site is live in production and downtime isn't acceptable Option 1: new Graph instance
Site isn't live yet, or a maintenance window is acceptable Option 2: smooth rebuild
Several external consumers have the single key hardcoded Option 1: forced key rotation surfaces every stale config for you
You want zero changes for external Graph consumers Option 2: credentials never change

Either way, don't treat this as a side detail of the CMS 13 upgrade. It's the part of the migration most likely to take down search, navigation, or personalization on your site if you discover it mid-deployment instead of during planning.

Links

October 01, 2026 →