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
awaitfor 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.


