Ethan Schofer
Dec 31, 2024
  108
(0 votes)

Managing Your Graph Conventions

Recently, Optimizely released a Conventions API for manging how various fields on your CMS content are indexed by the Graph. This is an extremely useful update as it allows us to much more easily manage what gets indexed and how it gets indexed: https://docs.developers.optimizely.com/content-management-system/docs/conventions-api.

Generally speaking, you manage this in the application Configure method in Startup.cs. According to the documentation, you create a service scope, get the conventions repository, and configure the fields on a given block or page.

using (var serviceScope = app.ApplicationServices.CreateScope())
{
    var services = serviceScope.ServiceProvider;
    var conventionRepo = services.GetRequiredService<ConventionRepository>();
    conventionRepo.ForInstancesOf<StandardPage>()
        .ExcludeField(p => p.ContentAreaItem1) //exclude a field
        .IncludeField(p => p.TermsAndConditions()) //add a dynamic field
        //set indexing type for specific properties
        .Set(p => p.Price, IndexingType.Queryable)
        .Set(p => p.Heading, IndexingType.Searchable)
        .Set(p => p.Quantity, IndexingType.OnlyStored);

    conventionRepo.ExcludeContentType<StartPage>(); //exclude specific content types

    conventionRepo.ExcludeAllContentTypes() //exclude all content types except a few
        .Except<OrderBlock>();

    conventionRepo
        .IncludeAbstract<MyAbstractPage2>() //add interface and abstract types to schema
        .IncludeAbstract<MyAbstractPage>()
        .IncludeInterface<ISearchPage>();
}

And this is excellent functionality. Fine grained control over my Graph indexing.

The problem arises when we start to think of this in the context of a large site. Are we going to add hundreds of lines of code to our Startup.cs? Do we have to touch Startup.cs each time we want to adjust our conventions? This all seems like a maintenance issue. We want to make this whiole process much easier to maintain. Specifically:

  1. Our code is organized in feature folders. Can we organize our conventions in this way as well?
  2. How can we add new pages or blocks, as well as the relevant conventions, without having to touch a whole bunch of code every time?
  3. How can we adjust existing pages or blocks and their relevant conventions and ensure our changes don't break anything?

Our initial idea was to manage these conventions similar to how Entity Framework manages conventions (https://learn.microsoft.com/en-us/ef/core/modeling/) using IEntityTypeConfiguration. We want our convention classes to be pulled into the Graph conventions automatically, so that if we make a new file of conventions, it will automatically be included, and we dont have to worry about missing a step.

Create an Interface

The first step is to create an interface that all of my conventions will use. Then we can do some work on any class that implements this interface. 

/// <summary>
/// Interface for conventions for each type
/// </summary>
public interface IGraphTypeConventions
{
    /// <summary>
    /// Method for adding the conventions
    /// </summary>
    /// <param name="conventionRepository">The graph conventions repository</param>
    public void Configure(ConventionRepository conventionRepository);
}

Any class we create that contains conventions must implement this interface. An example implementaion might look like this:

/// <summary>
/// Conventions for the accordion blocks
/// </summary>
public class AccordionGraphConventions : IGraphTypeConventions
{
    /// <summary>
    /// Adds conventions for the accordion blocks
    /// </summary>
    /// <param name="conventionRepository">The conventions repository</param>
    public void Configure(ConventionRepository conventionRepository)
    {
        // Ensure we have the repository
        ArgumentNullException.ThrowIfNull(conventionRepository);

        // Configure the accordion grouping block
        conventionRepository?.ForInstancesOf<AccordionGroupingBlock>()?
            .Set(p => p.Title, IndexingType.Searchable)
            .Set(p => p.Description, IndexingType.OnlyStored)
            .Set(p => p.IsFaq, IndexingType.Queryable)
            .Set(p => p.Items, IndexingType.OnlyStored)
            .ExcludeField(p => p.UniqueId);

        // Configure the accordion item block
        conventionRepository?.ForInstancesOf<AccordionItemBlock>()?
            .Set(p => p.Title, IndexingType.Searchable)
            .Set(p => p.Text, IndexingType.OnlyStored)
            .ExcludeField(p => p.UniqueId);
    }
}

Where we:

  1. Check that we have the repository
  2. Configure the AccordionGroupingBlock
  3. Configure the AccordionItemBlock

In a large site, we would expect to have at least one implementation of IGraphTypeConventions for each feature.

Register Instances of the Interface

In Startup.cs, in ConfigureServices, we want to add all of our conventions to the Depdency Injection container. So, right after we configure the graph, we call a new ServiceCollection extension method called RegisterAllGraphConventions where we get the current assembly, get all implementations of IGraphConvention from the assembly, and add them to the DI container. 

*Note: This is predicated on our architecture, where everything is in a single DLL. If your solution is spread out over multiple DLLs youi may need to adjust this to ensure you pick up all the IGraphTypeConventions.

/// <summary>
/// Extension method to register all graph convention classes
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Register all instances of IGraphTypeConventions
    /// </summary>
    /// <param name="services"></param>
    public static void RegisterAllGraphConventions(this IServiceCollection services)
    {
        // Get the current assembly - everything is in a single assembly - this aassembly
        var currentAssembly = Assembly.GetExecutingAssembly();

        // Get all graph convention classes from the assembly
        var typesFromAssembly =
            currentAssembly.DefinedTypes.Where(x => x.GetInterfaces().Contains(typeof(IGraphTypeConventions)));

        // Loop through the types
        foreach (var type in typesFromAssembly)
        {
            // Register them in the DI container
            services.Add(new ServiceDescriptor(typeof(IGraphTypeConventions), type, ServiceLifetime.Transient));
        }
    }
}

The advantage here is that if we add new conventions, they will automatically get registered. We can then easily call this from Startup.cs:

// Add graph conventions to the DI container
services.RegisterAllGraphConventions();

Add Conventions to the Conventions Repository

Lastly, we want to actually add these conventions to the ConventionsRepository. We do this in the application Configure method in Startup.cs. We create an extention method on IApplicationBuilder that will actually perform the registeration. In that method, we get the service provider and then get the ConventionRepository, as described in their documentation. But then we get all instances of IGraphTypeConventions that we registered in the DI container, loop through all of them and call the Configure method on each instance.

/// <summary>
/// Application builder extensions to register all graph conventions
/// </summary>
public static class GraphConventions
{
    /// <summary>
    /// Add all graph conventions to the convention repository
    /// </summary>
    /// <param name="app"></param>
    public static void AddGraphConventions(this IApplicationBuilder app)
    {
        // Get the scope
        using var serviceScope = app.ApplicationServices.CreateScope();

        // Get the service provider
        var services = serviceScope.ServiceProvider;

        // Get the convention repository
        var conventionRepository = services.GetRequiredService<ConventionRepository>();

        // If we don't have the repository
        if (conventionRepository == null)
        {
            // Just move on
            return;
        }

        // Get all the graph conventions
        var graphConventions = services.GetAllInstances<IGraphTypeConventions>();

        // Loop through the conventions
        foreach (var graphConvention in graphConventions)
        {
            // Call the configure method on each graph convention
            graphConvention.Configure(conventionRepository);
        }
    }
}

This will ensure that all conventions are registered, and it makes adding new conventions pretty easy. This would be added in Configure like this:

// Configure graph sync
app.AddGraphConventions();

Testing

Lastly, I want to be able to test these conventions so that if they get changed in the future, the test will fail until the test is also updated, ensuring we are adding conventions correctly. For testing, I am using XUnit, NSubstitute and FluentAssertions, so if you use different tools, the syntax of these tests may be different, but the concept should remain the same.

  1. Create a Substitute of the ConventionsRepository
  2. Create an instance of you conventions class
  3. Call the configure method
  4. Get the field conventions for this particular content type
  5. Review the conventions

[Fact]
public void Configure_ConfigureAccordionGroupingBlock_ConfigurationIsSet()
{
    // Arrange
    var conventionRepository = Substitute.For<ConventionRepository>();
    var accordionGraphConventions = new AccordionGraphConventions();

    // Act
    accordionGraphConventions.Configure(conventionRepository);

    // Assert
    var fieldConventions = conventionRepository.GetFieldConventions(typeof(AccordionGroupingBlock));

    foreach (var conventionType in fieldConventions)
    {
        var excludedFields = conventionType.GetExcludedFields();
        var excludedFieldsArray = excludedFields as string[] ?? excludedFields.ToArray();
        excludedFieldsArray.Length.Should().Be(1);
        excludedFieldsArray.FirstOrDefault().Should().Be(nameof(AccordionGroupingBlock.UniqueId));

        var indexedFields = conventionType.GetFieldsWithIndexingSetting();
        var indexedFieldsArray = indexedFields as string[] ?? indexedFields.ToArray();
        indexedFieldsArray.Length.Should().Be(4);
        indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Title));
        indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Description));
        indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.IsFaq));
        indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Items));

        conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Title)).Should().Be(IndexingType.Searchable);
        conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Description)).Should().Be(IndexingType.OnlyStored);
        conventionType.GetIndexingType(nameof(AccordionGroupingBlock.IsFaq)).Should().Be(IndexingType.Queryable);
        conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Items)).Should().Be(IndexingType.OnlyStored);
    }
}

So, we can bu sure that any of the following will break the test, requring you to examine your changes to ensure they are correct:

  • Changing the number and/or field that is excluded from convetions
  • Change the number and/or field included in conventions
  • Change the indexing type for a given field

Conclusions

All in all, the Conventions API is a nice feature from Optimizely to help us manage how the Graph indexes our content. Using the mthod outlined above will result in easy to maintain conventions that scale along with your solution.

Dec 31, 2024

Comments

Please login to comment.
Latest blogs
How to add an Admin Mode add-on in Optimizely CMS12

How to add a new add-on with navigation and unified stylesheet

Bartosz Sekula | Jan 2, 2025 | Syndicated blog

SaaS CMS and Visual Builder - Opticon 2024 Workshop Experience

Optimizely is getting SaaSy with us…. This year Optimizely’s conference Opticon 2024 took place in San Antonio, Texas. There were a lot of great...

Raj Gada | Dec 30, 2024

Copy Optimizely SaaS CMS Settings to ENV Format Via Bookmarklet

Do you work with multiple Optimizely SaaS CMS instances? Use a bookmarklet to automatically copy them to your clipboard, ready to paste into your e...

Daniel Isaacs | Dec 22, 2024 | Syndicated blog