---
title: Migrate an Alloy site from CMS 12 to CMS 13
slug: cms-13/docs/migrate-alloy-12-to-13
docTags: 
createdAt: 2026-07-30T20:22:07.892Z
---

This guide describes how to migrate an Optimizely Alloy site from Search & Navigation to Optimizely Graph with the Graph C# SDK.

The migration replaces the Search & Navigation client and APIs with Graph equivalents while preserving search functionality in your site.

## Migration overview

An Alloy site running on CMS 12 typically uses Optimizely Search & Navigation for site search. Search & Navigation is deprecated and replaced by Optimizely Graph, a GraphQL-based search platform with a C# SDK.

The migration involves the following changes:

| Area            | CMS 12 (Search & Navigation)           | CMS 13 (Graph)                                              |
| --------------- | -------------------------------------- | ----------------------------------------------------------- |
| NuGet packages  | `EPiServer.Find`, `EPiServer.Find.Cms` | `Optimizely.Graph.Cms.Query`, `Optimizely.Graph.AspNetCore` |
| Search client   | `IClient` / `SearchClient.Instance`    | `IGraphContentClient`                                       |
| Namespaces      | `EPiServer.Find.*`                     | `Optimizely.Graph.Cms.Query`                                |
| Configuration   | `EPiServer:Find`                       | `Optimizely:Graph`                                          |
| Execution model | Mostly synchronous                     | Asynchronous (async / await)                                |

## Prerequisites

Before you begin the migration, confirm the following:

- The Alloy site builds and runs on CMS 12.
- You use Git to manage source control.
- You have access to the following Optimizely Graph credentials:
  - Gateway address
  - App key
  - Secret
  - Single key

Create a branch before starting the migration:

```bash
git checkout -b feature/graph-migration
```

## Upgrade from CMS 12 to CMS 13

Upgrade the CMS platform before replacing Search & Navigation with Graph. This isolates the platform upgrade from the search migration and simplifies troubleshooting.

### Update CMS packages

Open the `.csproj` project file and update the CMS packages to version 13.

```xml
<!-- CMS 12 -->
<PackageReference Include="EPiServer.CMS" Version="12.*" />
<PackageReference Include="EPiServer.CMS.AspNetCore" Version="12.*" />

<!-- CMS 13 -->
<PackageReference Include="EPiServer.CMS" Version="13.*" />
<PackageReference Include="EPiServer.CMS.AspNetCore" Version="13.*" />
```

Update any additional `EPiServer.*` packages to versions compatible with CMS 13.

### Restore and build the project

Run the following commands:

```bash
dotnet restore
dotnet build
```

Resolve any build errors before continuing.

Keep the `EPiServer.Find` packages installed at this stage. Confirm that the CMS upgrade succeeds before changing the search implementation.

## Verify the CMS upgrade

After upgrading to CMS 13, verify that the site functions correctly before modifying the search implementation.

### Run the site

```bash
dotnet run
```

### Verification checklist

Confirm the following:

- The site starts without errors.
- The CMS edit interface loads correctly.
- Content pages render correctly.
- The existing Search & Navigation search implementation still works.
- Application logs contain no errors.

If automated tests exist, run them:

```bash
dotnet test
```

Commit the CMS upgrade when verification succeeds.

```bash
git add -A
git commit -m "chore: upgrade CMS 12 to CMS 13"
```

Upgrading the CMS separately makes Search & Navigation issues easier to identify and resolve.

## Replace Search & Navigation NuGet packages

Remove the Search & Navigation packages and install the Graph packages.

### Remove Search & Navigation packages

Remove the following packages from the project file:

```xml
<PackageReference Include="EPiServer.Find" />
<PackageReference Include="EPiServer.Find.Cms" />
<PackageReference Include="EPiServer.Find.Framework" />
```

If the project contains the following package, remove it:

```xml
<PackageReference Include="Wangkanai.Detection" />
```

### Add Graph packages

Add the Graph packages:

```xml
<PackageReference Include="Optimizely.Graph.Cms.Query" Version="3.*" />
<PackageReference Include="Optimizely.Graph.AspNetCore" Version="3.*" />
```

Restore packages:

```bash
dotnet restore
```

:::hint{type="info"}
**Note**
Build errors may appear at this stage because the code still references Search & Navigation APIs.
:::

## Update configuration

Remove the Search & Navigation configuration and add Graph configuration in `appsettings.json`.

### Remove Search & Navigation configuration

```json
"EPiServer": {
  "Find": {
    "ServiceUrl": "https://...",
    "DefaultIndex": "YOUR_INDEX_NAME"
  }
}
```

### Add Graph configuration

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

Retrieve these values from the Graph management portal.

## Update service registration

Update `Startup.cs` to remove Search & Navigation services and register Graph services.

### Remove Search & Navigation services

Remove the following service registrations:

- `services.AddFind()`
- `services.AddDetection()`
- `app.UseDetection()`

### Register Graph services

Add the following registrations:

```csharp
services.AddContentGraph();
services.AddGraphContentClient();
services.AddVisitorGroupsCore();
```

Add Graph middleware:

```csharp
app.UseGraphTrackingScripts();
```

## Update namespaces

Replace Search & Navigation namespaces with Graph namespaces.

| Search & Navigation namespace | Graph namespace              |
| ----------------------------- | ---------------------------- |
| `EPiServer.Find`              | `Optimizely.Graph.Cms.Query` |
| `EPiServer.Find.Api`          | `Optimizely.Graph.Cms.Query` |
| `EPiServer.Find.Cms`          | `Optimizely.Graph.Cms.Query` |
| `EPiServer.Find.Framework`    | `Optimizely.Graph.Cms.Query` |

Remove unused namespaces such as:

- `EPiServer.Find.Framework.Statistics`
- `EPiServer.Find.Helpers.Text`
- `EPiServer.Find.UI`
- `Wangkanai.Detection.*`

## Replace the search client

Search & Navigation uses `IClient` or `SearchClient.Instance`.

Graph uses `IGraphContentClient`, which is registered for dependency injection.

### Example: Search & Navigation client

```csharp
private readonly IClient _client;

public SearchService(IClient client)
{
    _client = client;
}
```

### Example: Graph client

```csharp
private readonly IGraphContentClient _client;

public SearchService(IGraphContentClient client)
{
    _client = client;
}
```

## Create a query interface

Graph queries use interfaces instead of concrete page types.

Create an interface that represents searchable content.

Example:

```csharp
[ContentType]
public interface ISitePageData : IContent
{
    string MetaTitle { get; set; }
    string TeaserText { get; set; }
}
```

Update the base page type to implement the interface:

```csharp
public abstract class SitePageData : PageData, ISitePageData
{
}
```

## Rewrite search queries

Replace Search & Navigation query APIs with Graph query APIs.

### API mapping

| Search & Navigation API | Graph API                   |
| ----------------------- | --------------------------- |
| `.Search<T>()`          | `.QueryContent<T>()`        |
| `.For()`                | `.SearchFor()`              |
| `.Filter()`             | `.Where()`                  |
| `.Take()`               | `.Limit()`                  |
| `.GetResult()`          | `await GetAsContentAsync()` |
| `.TotalMatching`        | `.Total`                    |

Graph queries require asynchronous execution.

### Example query

```csharp
var result = await _client.QueryContent<ISitePageData>()
    .SearchFor(query)
    .UsingFullText()
    .Limit(pageSize)
    .Skip((page - 1) * pageSize)
    .IncludeTotal()
    .GetAsContentAsync();
```

## Update asynchronous methods

Optimizely Graph queries are asynchronous. Update calling methods to support async execution.

### Example

Before:

```csharp
public ActionResult Search(string q)
{
    var results = _searchService.Search(q);
    return View(results);
}
```

After:

```csharp
public async Task<ActionResult> Search(string q)
{
    var results = await _searchService.SearchAsync(q);
    return View(results);
}
```

## Replace `UnifiedSearch`

If the site uses `UnifiedSearch`, replace it with a Graph query that uses the interface created earlier.

Example:

```csharp
var result = await _client.QueryContent<ISitePageData>()
    .SearchFor(query)
    .UsingFullText()
    .Limit(pageSize)
    .Skip(offset)
    .IncludeTotal()
    .GetAsContentAsync();
```

## Update Razor views

Update Razor views to register Graph tag helpers and enable click tracking in the browser.

Add the Graph tag helper:

```csharp
@addTagHelper *, Optimizely.Graph.AspNetCore
```

Add tracking to the layout:

```html
<graph-tracking-setup />
```

If you use click tracking, update search result links:

```html
<graph-trackable-link url="@Url.ContentUrl(hit.ContentLink)"
track-url="@Model.GetTrackUrl(hit)">
@hit.Name
</graph-trackable-link>
```

## Remove remaining Search & Navigation code

Search the project for any remaining Search & Navigation references and remove them.

Examples include:

- `FindOptions`
- `IFindStatisticsClient`
- `SearchSection`
- Search & Navigation initialization modules

## Build and test

Verify the migration by building, running tests, and starting the site.

Build the project:

```bash
dotnet build
```

Run tests:

```bash
dotnet test
```

Start the site and verify that search works correctly:

```bash
dotnet run
```

## Troubleshoot common issues

Use the following solutions to resolve common problems after migration.

### Search returns no results

Verify the following:

- Graph credentials in `appsettings.json` are correct.
- `services.AddContentGraph()` is registered.
- Content has synchronized to Optimizely Graph.

Inspect the generated GraphQL query:

```csharp
var query = _client.QueryContent<ArticlePage>().Limit(10);
Console.WriteLine(query.ToGraphQL());
```

### Total results return zero

Confirm the query includes:

```csharp
.IncludeTotal()
```

Without this method, the total count is not returned.
