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:
- Our code is organized in feature folders. Can we organize our conventions in this way as well?
- 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?
- 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:
- Check that we have the repository
- Configure the AccordionGroupingBlock
- 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.
- Create a Substitute of the ConventionsRepository
- Create an instance of you conventions class
- Call the configure method
- Get the field conventions for this particular content type
- 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.
Comments