Mardy
Opal Tools CertificationOpal Administrator Certificationworkspace_premium +4
Oct 14, 2026
visibility 26
star star star star star
(1 votes)

Computed Fields in CMS 13 Content Contracts with Optimizely Graph

Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph.

But what if the shared field doesn't exist as a stored CMS property, and its value needs to be calculated differently depending on the content type?

For example, imagine a search feature that includes articles, events, and images:

  • Articles use Heading as their search title.
  • Events use EventName.
  • Images use ImageTitle, falling back to their name.

We want all three to expose a common SearchTitle field in Graph, without adding another persisted property to each content type.

Here's a working approach tested with Optimizely CMS 13.3.0.

1. Define a Content Contract

First, define a Content Contract with a method that each implementing content type uses to provide its search title.

[ContentType]
public interface ISearchableContent : IContentData
{
    string GetSearchTitle();
}

Each content type implements the same contract but calculates the search title using its own properties. For example, an article page uses Heading, while an event page uses EventName.

public class ArticlePage : PageData, ISearchableContent
{
    public virtual string Heading { get; set; }

    public string GetSearchTitle() =>
        string.IsNullOrWhiteSpace(Heading) ? Name : Heading;
}
public class EventPage : PageData, ISearchableContent
{
    public virtual string EventName { get; set; }

    public string GetSearchTitle() =>
        string.IsNullOrWhiteSpace(EventName) ? Name : EventName;
}

Both content types implement the same contract but calculate SearchTitle differently. The same approach works for media types, such as images, using their own properties.

Make sure the relevant content types implement or are associated with the Content Contract in CMS so they can be queried together in Graph.

This keeps the calculation close to each content type and avoids maintaining a central switch statement.

2. Expose the Computed Field in Graph

Next, create an extension method and register it using Graph indexing conventions.

public static class SearchExtensions
{
    public static string SearchTitle(this ISearchableContent content)
        => content.GetSearchTitle();
}

Register the computed field:

services.AddContentGraph(configureConventions: conventions =>
{
    conventions.ForInstancesOf<ISearchableContent>()
        .IncludeField(
            x => x.SearchTitle(),
            IndexingType.Searchable);
});

The important detail is that SearchTitle is computed during indexing. We don't need to add a persisted SearchTitle property to our CMS models.

The resulting Graph schema exposes the field through ISearchableContent, regardless of the underlying content type.

3. Query It with the Graph C# SDK

The interesting part is that the computed field can also be used in strongly typed SDK queries.

First, define a lightweight result model:

public class SearchResult
{
    public string SearchTitle { get; set; }
}

Then query the Content Contract:

var query = graphClient
    .QueryContent<ISearchableContent>()
    .Where(x => x.SearchTitle().Contains("Optimizely"))
    .Limit(20)
    .IncludeTotal()
    .Fields<SearchResult>(x => x.SearchTitle);

var result = await query.GetAsync();

In our test, the SDK translated the filtering expression into GraphQL:

query ISearchableContentQuery {
  ISearchableContent(
    where: { SearchTitle: { contains: "Optimizely" } }
    limit: 20
  ) {
    items {
      SearchTitle
    }
    total(all: true)
  }
}

The query returned one matching event with SearchTitle equal to Optimizely Developer Webinar.

Here's the interesting detail: SearchTitle() is a C# extension method, not a stored CMS property. Graph indexing conventions expose it as a searchable field, and the Graph C# SDK translates the expression into a server-side GraphQL filter rather than filtering an in-memory collection.

We also verified sorting by the computed field, language-specific values, and recalculation after updating and publishing the underlying content.

Key Takeaway

By combining CMS 13 Content Contracts, Graph indexing conventions, and the Graph C# SDK, we can expose a shared searchable field across different content types while keeping the calculation specific to each type.

No additional persisted CMS properties are required.

Keep computed field calculations lightweight and deterministic, since they run as part of the indexing process.

References

Oct 14, 2026

Comments

Johan Kronberg
Johan Kronberg Oct 14, 2026 06:39 AM

It also works well having a body on the interface method making it a default interface method:

string GetSearchTitle() { return "x"; }

Quite nice to not have to implement an actual method everywhere the interface is added...

error Please login to comment.
Latest blogs
Opal Workflow Agents And The Graph Webhook Trap

What we did? We built a compliance review workflow agent in Opal. The job was simple: when an editor submits a page for review in Optimizely CMS 13...

Ritu Madan | Oct 14, 2026

cms13-scan: A Deterministic CMS 13 Readiness Scanner

Every CMS 12 to CMS 13 upgrade I have looked at starts the same way. Someone opens the solution and starts hunting. A text search for the APIs

Adnan Zameer | Oct 12, 2026 |

Setting Up an Optimizely CMS 13 MCP Server Locally with Codex

Optimizely CMS 13 supports Model Context Protocol (MCP) , allowing AI tools such as Claude and Codex to interact with CMS content and metadata...

Mardy | Oct 12, 2026