---
title: Indexing conventions
slug: cms-13/docs/indexing-conventions
docTags: 
createdAt: 2026-07-30T20:22:09.287Z
---

:::hint{type="info"}
**Note**
Indexing conventions require CMS 13 version 13.1.0 or later.
:::

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

Conventions apply only to content that the **SearchIndexer** role can read. Optimizely Graph does not index content without **SearchIndexer** read access, regardless of convention settings. CMS does not add the **SearchIndexer** role to custom access rights. If you clear **Inherit settings from parent item** on a content item, give the **SearchIndexer** role read access. See [Verify SearchIndexer access rights](docId:5IIpWhRi9sd4hvXlqyaHY).

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

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

:::hint{type="info"}
**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`:

```csharp
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.

:::hint{type="info"}
**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`:

```csharp
[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:

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

Register the computed field in the `configureConventions` callback:

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

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

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

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

```csharp
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.

```csharp
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:

```csharp
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.

```csharp
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:

```csharp
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:

| Signature                                                                                      | Notes                         |
| ---------------------------------------------------------------------------------------------- | ----------------------------- |
| `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](docId\:nXwTyhMMzOa_EuQRBG3sZ) section.

| Return type                  | Graph type    | Example                 |
| ---------------------------- | ------------- | ----------------------- |
| `string`                     | `String`      | `"hello"`               |
| `int`, `long`                | `Int`         | `42`                    |
| `float`, `double`, `decimal` | `Float`       | `3.14`                  |
| `bool`                       | `Boolean`     | `true`                  |
| `DateTime`                   | `DateTime`    | `DateTime.UtcNow`       |
| `DateTimeOffset`             | `DateTime`    | `DateTimeOffset.UtcNow` |
| `XhtmlString`                | Rich text     | Rendered HTML           |
| `IEnumerable<T>`             | `[T]`         | `["a", "b"]`            |
| `List<T>`                    | `[T]`         | `[1, 2, 3]`             |
| POCO class                   | Component     | Nested object           |
| `List<TPoco>`                | `[Component]` | List of nested objects  |
| Nullable value types         | Same as base  | `int?` → `Int`          |

:::hint{type="danger"}
**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.

```csharp
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:

```csharp
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:

```graphql
{
  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.

```csharp
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:

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

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

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

### Control indexing on POCO properties

Control POCO property indexing with `IndexingTypeAttribute`:

```csharp
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:

```csharp
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.

```csharp
[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:

```csharp
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`.

```csharp
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.

:::hint{type="info"}
**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.
