HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Indexing conventions

Configure indexing conventions to customize how content types are indexed to Optimizely Graph, including computed fields, type exclusions, and indexing types.

Configure indexing conventions to customize how content types are indexed to Optimizely Graph. Use the configureConventions parameter in AddContentGraph during application startup.

services.AddContentGraph(configureConventions: conventions =>
{
    conventions.ForInstancesOf<ProductPage>()
        .IncludeField(p => p.TotalPrice())
        .IncludeField(p => p.ComputedTitle(), IndexingType.Searchable);

    conventions.ExcludeContentType<InternalPage>();
});
📘

Note

Content types and content data are not resynced automatically when conventions change and the site restarts. After changing conventions in code and redeploying the site, re-run the Optimizely Graph Full Synchronization job so data is consistent with your configurations. Not doing so results in unpredictable outcomes.

Exclude content types

Exclude content types from syncing to the Optimizely Graph schema. Optimizely Graph does not index content of excluded content types.

The following example excludes InternalPage and DraftBlock:

conventions.ExcludeContentType<InternalPage>()
    .ExcludeContentType<DraftBlock>();

When excluded, InternalPage and DraftBlock are removed from the GraphQL schema, and their content is not synced to Optimizely Graph.

When you add content of excluded content types to a ContentArea or ContentAreaItem property of another type, those items are not included in the indexed content area.

Computed fields

Indexing conventions let you add computed fields to content types at indexing time without modifying the content model itself. Computed fields are methods whose return values are indexed alongside regular CMS properties.

📘

Note

Define computed fields as extension methods rather than instance methods on the content type. This keeps the content model clean and separates indexing logic from the domain model. Extension methods are the recommended approach for computed fields.

Define computed fields

Define computed fields as extension methods in a separate static class. This keeps indexing logic separate from the content model.

The following example uses a page type called ProductPage:

[ContentType]
public class ProductPage : PageData
{
    public virtual string ProductName { get; set; }
    public virtual int Quantity { get; set; }
    public virtual double Price { get; set; }
}

Define the computed field as an extension method:

public static class ProductPageComputedFields
{
    public static double TotalPrice(this ProductPage page)
    {
        return page.Quantity * page.Price;
    }
}

Register the computed field in the configureConventions callback:

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.TotalPrice());

After ProductPage syncs to Optimizely Graph, query TotalPrice using the following:

{
  ProductPage {
    items {
      ProductName
      Quantity
      Price
      TotalPrice
    }
  }
}

Specify the indexing type for the computed field as the second parameter of IncludeField:

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.TotalPrice(), IndexingType.Queryable);

Computed fields with service injection

Computed fields can accept an IServiceProvider parameter for accessing application services at indexing time. Pass null! for the parameter in the registration expression; the framework injects the real service provider at invocation time.

public static class ProductPageComputedFields
{
    public static int ComputedChildCount(this ProductPage page, IServiceProvider sp = null!)
    {
        var loader = sp.GetRequiredService<IContentLoader>();
        return loader.GetChildren<IContent>(page.ContentLink).Count();
    }
}

Register the computed field in the configureConventions callback:

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.ComputedChildCount());

Async computed fields

For I/O-bound operations, computed fields can return Task<T>. Async methods must accept a CancellationToken parameter. Use default for the cancellation token in the registration expression.

public static class ProductPageComputedFields
{
    public static Task<string> ComputedSlugAsync(
        this ProductPage page,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult(
            page.ProductName?.ToLowerInvariant().Replace(' ', '-') ?? "untitled");
    }
}

Register the computed field in the configureConventions callback:

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.ComputedSlugAsync());

Supported method signatures

The framework auto-detects IServiceProvider and CancellationToken parameters. The expression passed to IncludeField is used only as a method selector. Argument values are discarded and injected by the framework at invocation time.

The following method signatures are supported:

SignatureNotes
T Method()Simple synchronous
T Method(IServiceProvider sp)With service injection
Task<T> MethodAsync(CancellationToken ct)Async
Task<T> MethodAsync(IServiceProvider sp, CancellationToken ct)Async with services
static T Method()Static method
static T Method(this TContent content)Extension method
static T Method(this TContent content, IServiceProvider sp)Extension with services
static Task<T> MethodAsync(this TContent content, CancellationToken ct)Async extension
static Task<T> MethodAsync(this TContent content, IServiceProvider sp, CancellationToken ct)Async extension with services

Supported return types

The following return types are supported for computed fields. POCO (plain C# object) return types are covered in the POCO components section.

Return typeGraph typeExample
stringString"hello"
int, longInt42
float, double, decimalFloat3.14
boolBooleantrue
DateTimeDateTimeDateTime.UtcNow
DateTimeOffsetDateTimeDateTimeOffset.UtcNow
XhtmlStringRich textRendered HTML
IEnumerable<T>[T]["a", "b"]
List<T>[T][1, 2, 3]
POCO classComponentNested object
List<TPoco>[Component]List of nested objects
Nullable value typesSame as baseint?Int
❗️

Warning

Use simple, fast methods to avoid issues when syncing content to Optimizely Graph.

Use IncludeField to add data to a content item that does not belong to CMS content. Do not return CMS data such as ContentReference, ContentArea, Blob, or BlockData because many features associated with those data types do not work with computed fields.

IncludeField does not support methods that return the following CMS types:

  • Blob
  • ContentArea
  • ContentAreaItem
  • ContentReference
  • LinkItem
  • LinkItemCollection
  • PageData
  • BlockData
  • MediaData

Computed fields and type inheritance

Computed fields configured on a content type automatically appear on all derived content types. Register a computed field on a base type to include it on every content type that inherits from it when indexed to Optimizely Graph.

public class BasePage : PageData { }

public class ArticlePage : BasePage
{
    public virtual string Heading { get; set; }
}

public class NewsPage : BasePage
{
    public virtual DateTime PublishDate { get; set; }
}

public static class BasePageComputedFields
{
    public static string ComputedSection(this BasePage page) => "default";
}

Register the computed field on the base type:

conventions.ForInstancesOf<BasePage>()
    .IncludeField(p => p.ComputedSection());

ArticlePage and NewsPage include ComputedSection in their indexed content without additional configuration. Query it on any of the derived types:

{
  ArticlePage {
    items {
      Heading
      ComputedSection
    }
  }
  NewsPage {
    items {
      PublishDate
      ComputedSection
    }
  }
}

POCO components

When a computed field returns a POCO (plain C# object), the framework automatically registers it as a Graph component type and discovers its public properties.

public class ProductSummary
{
    public string Title { get; set; } = "";
    public decimal Price { get; set; }
    public bool InStock { get; set; }
}

public static class ProductPageComputedFields
{
    public static ProductSummary ComputedSummary(this ProductPage page) => new()
    {
        Title = page.ProductName,
        Price = (decimal)page.Price,
        InStock = true
    };
}

Register the computed field in the configureConventions callback:

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.ComputedSummary());

After ProductPage syncs to Optimizely Graph, query the nested component:

{
  ProductPage {
    items {
      ProductName
      ComputedSummary {
        Title
        Price
        InStock
      }
    }
  }
}

Control indexing on POCO properties

Control POCO property indexing with IndexingTypeAttribute:

public class ProductSummary
{
    [IndexingType(IndexingType.Searchable)]
    public string Title { get; set; } = "";

    [IndexingType(IndexingType.Queryable)]
    public decimal Price { get; set; }
}

Lists of POCOs

Computed fields can also return lists of POCOs:

public class ProductTag
{
    public string Name { get; set; } = "";
    public int Weight { get; set; }
}

public static class ProductPageComputedFields
{
    public static List<ProductTag> ComputedTags(this ProductPage page) =>
    [
        new() { Name = "electronics", Weight = 10 },
        new() { Name = "sale", Weight = 5 }
    ];
}

Multiple language support with computed fields

Give computed fields language-specific values by accessing the content item's language at indexing time. The following example returns a language-specific terms and conditions link.

[ContentType]
public class StandardPage : PageData { }

public static class StandardPageComputedFields
{
    public static string TermsAndConditions(this StandardPage page)
    {
        var language = page.Language.Name;
        var tncLink = "https://ourcorp/tnc";
        var tnc = "Terms and conditions";
        switch (language)
        {
            case "sv":
                tnc = "villkor";
                break;
        }
        return $"{tnc}: {tncLink}";
    }
}

Register the computed field in the configureConventions callback:

conventions.ForInstancesOf<StandardPage>()
    .IncludeField(p => p.TermsAndConditions());

When a StandardPage has multiple versions in different languages, the TermsAndConditions field returns a different value for each language version.

Set indexing type for fields

Use IndexingType to control how computed fields are indexed. Specify this as the second parameter of IncludeField.

conventions.ForInstancesOf<ProductPage>()
    .IncludeField(p => p.ComputedTitle(), IndexingType.Searchable)
    .IncludeField(p => p.TotalPrice(), IndexingType.Queryable);

The following three indexing types are available for indexing conventions:

  • Searchable – The property is searchable using full-text search. Applicable only to leaf properties of XhtmlString or primitive types (string, int, datetime, and so on).
  • Queryable – The property can be filtered using operators such as eq, gt, and gte, but is not searchable when using full-text search.
  • Default – The system default indexing behavior is applied.
📘

Note

To set the indexing type for existing CMS properties (not computed fields), use the Admin UI under Settings > Content Types. Select the content type, then configure the Property Indexing Type for each property.

Best practices

  1. Use extension methods – Define computed fields as extension methods in a separate static class to keep indexing logic separate from the content model. This improves testability and avoids polluting content types with indexing concerns.
  2. Use descriptive method names – The method name becomes the field name in GraphQL (for example, ComputedTitle becomes ComputedTitle in queries).
  3. Keep computations lightweight – Computed fields run during indexing for every content item. Avoid expensive operations.
  4. Use async for I/O – If accessing external services or data, use async methods with CancellationToken.
  5. Default parameters for injected values – Use = null! for IServiceProvider and = default for CancellationToken in method signatures.
  6. Avoid name collisions – Computed field names must not match existing CMS property names on the same content type.
  7. Prefer POCO components for structured data – Group related values into a POCO rather than adding many flat fields.