Extending Authoring and Management GraphQL API

Extending Authoring and Management GraphQL API

Extend Sitecore Authoring and Management GraphQL API to fetch related items. Learn how to register new query schema and test using GraphQL Playground.

Sitecore Authoring and Management API is a GraphQL endpoint that allows effective content managements trough predefined queries, mutation and subscriptions. It was released with Sitecore XP 10.3 version in December 2022 and is really handy in building custom Sitecore Content Management modules. While the API is robust and it is hard to find a feature that it does not support, there always will be some area that is not covered and a good reason to extend the API. For example, I could not find a predefined query to fetch related items. That encouraged me to explore the way to enrich the schema with such custom functionality.

In this article I am going to share how to extend the schema, define a related items query with required request and response models. At the end we are going to use in-built GraphQL playground to test the new query.

Headless Rendering GraphQL API vs Authoring and Management API

Before going further, let me point out that Sitecore XM/XP is featured by another GraphQL API which is used mostly for the needs of headless development of website components. It is provided with Sitecore Headless Rendering module as an extra installation package. The API schema and all related functionalities are included in DDL’s that matches the pattern Sitecore.Services.GraphQL.*.dll. Default configuration is included in config files under \App_Config\Sitecore\Services.GraphQL root. GraphQL implementation is based on GraphQL .NET library and extending that API is pretty well documented on Sitecore website.

From the other hand, Authoring and Management API is shipped by default with XM//XP files. The API schema and related functionalities are included in DLL’s that matches the pattern Sitecore.GraphQL.*.dll. Default configuration is included in config files under \App_Config\Sitecore\Sitecore.GraphQL root. GraphQL implementation is based on HotChocolate library. The schema is totally different than the one shipped with Headless Rendering Module. I could not find any documentation on Sitecore website how to extend it, however resources available on HotChocolate project website were useful.

Extending the Schema

The goal is to create a new schema definition for related items query. This includes:

  • defining GraphQL query input model

  • defining GraphQL query search result model

  • creation of class that contains the query execution logic

  • registration of new related items query in schema configuration

To move forward we need to create a .NET Framework Wep Application project. I named it SitecoreGroove.Feature.AuthoringAndManagementApi. We need to also include below dependencies (applicable to Sitecore 10.4 version):

Install-Package Sitecore.GraphQL.Core -Version 10.4.0 -Source https://sitecore.myget.org/F/sc-packages/api/v3/index.json -IgnoreDependencies
Install-Package Sitecore.Kernel -Version 10.4.0 -Source https://sitecore.myget.org/F/sc-packages/api/v3/index.json -IgnoreDependencies
Install-Package HotChocolate.Abstractions -Version 10.5.5 -IgnoreDependencies
Install-Package HotChocolate.Types -Version 10.5.5 -IgnoreDependencies

Next step is to modify web.config properties in the project to not include the file in web deploy package. Let’s change Build Action to None and Copy to Output Directory to Do not copy.

Now, let’s create RelatedItemsQueryInput.cs class in RelatedItems folder. The class will represent query parameters that will be used to query related items. It’s simple model annotated by HotChocolate [InputObjectType] attribute, thanks to which the framework will identify it as query parameters class. Each property of the class are annotated further by [GraphQLDescription] attribute. As we will se later, this description will be visible when browsing the schema or when using auto-complete in GraphQL Playground.

The RelatedItemsQueryInput class contains four properties:

  • ItemUri - unique identifier of Sitecore item for which the related items will be queried. Sitecore item URI contains item ID, version number and language name passed in specific format like sitecore://master/{CB6F6AE3-6554-41AC-8141-F12F62F4CE35}?lang=en&ver=1

  • FilterByWorkflowState - whether related items should be of the same workflow state as the item for which the query is executed

  • ExcludeItemsWithLayout - whether to exclude from search results items that have any presentation details defined

  • Resursive - whether to evaluate related items from related items until the last one

using HotChocolate;
using HotChocolate.Types;

namespace SitecoreGroove.Feature.AuthoringAndManagementApi.RelatedItems
{
    [InputObjectType]
    [GraphQLDescription("Represents a related items query input.")]
    public class RelatedItemsQueryInput
    {
        [GraphQLNonNullType]
        [GraphQLDescription("Sitecore item Uri")]
        public string ItemUri { get; set; }

        [GraphQLDescription("Indicates if related items should be of the same workflow step. If true and requested item is of 'Draft' state, then it will reurn 'Draft' related items only.")]
        public bool FilterByWorkflowState { get; set; }

        [GraphQLDescription("Indicates if related items should inlcude items that has any presentation details defined.")]
        public bool ExcludeItemsWithLayout { get; set; }

        [GraphQLDescription("If false then direct related items only are returned. If true then related items are evaluated from related item until the last one.")]
        public bool Resursive { get; set; }
    }
}

Next, let’s create RelatedItem.cs class in RelatedItems folder - it will represent a related item model. Similar as RelatedItemsQueryInput class, it should contains a number of properties decorated by [GraphQLDescription] attribute. At the end, the query will return a collection of RelatedItem objects.

using HotChocolate;
using System;

namespace SitecoreGroove.Feature.AuthoringAndManagementApi.RelatedItems
{
    [GraphQLDescription("Represents a related items search result.")]
    public class RelatedItem
    {
        [GraphQLDescription("Sitecore item Uri")]
        public string ItemUri { get; set; }

        [GraphQLDescription("Sitecore item name")]
        public string Name { get; set; }

        [GraphQLDescription("Sitecore item version")]
        public int Version { get; set; }

        [GraphQLDescription("Sitecore item language")]
        public string Language { get; set; }

        [GraphQLDescription("Sitecore item path")]
        public string Path { get; set; }

        [GraphQLDescription("Sitecore item template name")]
        public string TemplateName { get; set; }

        [GraphQLDescription("Sitecore item last modification date")]
        public DateTime Updated { get; set; }

        [GraphQLDescription("User name who last time modified given Sitecore item")]
        public string UpdatedBy { get; set; }

        [GraphQLDescription("Sitecore item workflow state")]
        public Guid WorkflowStateId { get; set; }
    }
}

We are missing the class that will utilize above models and will be responsible for querying related items. Let’s add to RelatedItems folder a ReleatedItems class. The class needs to be annotated by [ExtendObjectType(Name = "Query")] attribute to be recognized as the query by the framework.

In the constructor we can utilize any services registered in Sitecore DI ecosystem - in this case we will use BaseClient that allows getting an Item by providing Item URI.

The entry point of the query is GetRelatedItems(RelatedItemsQueryInput relatedItemsQuery) method which accepts RelatedItemsQueryInput model as parameter. It uses GetItemNotNull method of _baseClient to materialize Sitecore Item from Item URI. Next, we fetch related items by calling Links.GetAllLinks method of the item. Note that alternatively we could use Links Database to fetch related items, however I found Links.GetAllLinks to be way faster (it resolves related items by getting target items from all reference fields of the item).

Next, depending on the query input parameters, we filter by the workflow state, exclude items with presentation details and repeat fetching related items process for related items if only recursive flag is set to true.

using HotChocolate;
using HotChocolate.Types;
using Sitecore;
using Sitecore.Abstractions;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SitecoreGroove.Feature.AuthoringAndManagementApi.RelatedItems
{
    [ExtendObjectType(Name = "Query")]
    public class ReleatedItems
    {
        private readonly BaseClient _client;

        public ReleatedItems(BaseClient client)
        {
            _client = client;
        }

        [GraphQLDescription("Gets related items for requested item.")]
        public IEnumerable<RelatedItem> GetRelatedItems(RelatedItemsQueryInput relatedItemsQuery)
        {
            Item item = _client.GetItemNotNull(ItemUri.Parse(relatedItemsQuery.ItemUri));

            List<Item> allRealtedItems = new List<Item>();

            ResolveReferences(item, allRealtedItems, relatedItemsQuery.FilterByWorkflowState, relatedItemsQuery.ExcludeItemsWithLayout, relatedItemsQuery.Resursive);

            return allRealtedItems
                .OrderBy(x => x.Paths.FullPath)
                .Select(x => new RelatedItem
                {
                    ItemUri = x.Uri.ToString(),
                    Language = x.Language.Name,
                    Name = x.Name,
                    Path = x.Paths.FullPath,
                    TemplateName = x.TemplateName,
                    Updated = x.Statistics.Updated,
                    UpdatedBy = x.Statistics.UpdatedBy,
                    Version = x.Version.Number,
                    WorkflowStateId = Guid.Parse(x[FieldIDs.WorkflowState])
                });
        }

        private void ResolveReferences(Item item, List<Item> allReferences, bool filterByWorkflowState, bool excludeItemsWithLayout, bool recursive)
        {
            IEnumerable<Item> relatedItems = item.Links.GetAllLinks(false, true)
                .Select(x => x.GetTargetItem())
                .Where(x => x != null)
                .Where(x => x.Paths.IsContentItem || x.Paths.IsMediaItem);

            if (excludeItemsWithLayout)
            {
                relatedItems = relatedItems.Where(x => string.IsNullOrEmpty(LayoutField.GetFieldValue(x.Fields[FieldIDs.LayoutField])));
            }

            if (filterByWorkflowState)
            {
                relatedItems = relatedItems.Where(x => x[FieldIDs.WorkflowState] == item[FieldIDs.WorkflowState]);
            }

            foreach (Item relatedItem in relatedItems)
            {
                if (allReferences.Any(x => x.ID == relatedItem.ID))
                {
                    continue;
                }

                allReferences.Add(relatedItem);

                if (recursive)
                {
                    ResolveReferences(relatedItem, allReferences, filterByWorkflowState, excludeItemsWithLayout, recursive);
                }
            }
        }
    }
}

OK, the query is ready now. We just need to register it. Let’s find out where the out-off-the-box schema is configured.

If we have a look into \App_Config\Sitecore\Sitecore.GraphQL\Sitecore.GraphQL.config file we will find below schema definition:

<schemaRegistrations type="Sitecore.GraphQL.NetFxHost.SchemaRegistrationSource, Sitecore.GraphQL.NetFxHost">
    <param desc="registrations" hint="list:">
        <meta type="Sitecore.GraphQL.Schema.Meta.MetaSchemaRegistration, Sitecore.GraphQL.Schema" />
        <authoring type="Sitecore.GraphQL.Schema.Authoring.AuthoringSchemaRegistration, Sitecore.GraphQL.Schema" />
        <management type="Sitecore.GraphQL.Schema.Management.ManagementSchemaRegistration, Sitecore.GraphQL.Schema" />
    </param>
</schemaRegistrations>

If we decompile Sitecore.GraphQL.Schema.dll and look into Sitecore.GraphQL.Schema.Authoring.AuthoringSchemaRegistration class we will notice that it’s signature is internal and it inherits from internal abstract AuthoringSchemaRegistration class, which implements ISchemaRegistration interface.

Given the mentioned classes are internal, the right ways to extend the schema seems to be creation of new schema registration class that implement ISchemaRegistration and registering it in <schemaRegistrations> configuration patch like below.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore role:require="Standalone or ContentManagement or XMCloud">
        <authoring>
            <graphql>
                <schemaRegistrations type="Sitecore.GraphQL.NetFxHost.SchemaRegistrationSource, Sitecore.GraphQL.NetFxHost">
                    <param desc="registrations" hint="list:">
                        <sitecoreGroove type="SitecoreGroove.Feature.AuthoringAndManagementApi.SchemaRegistration, SitecoreGroove.Feature.AuthoringAndManagementApi" />
                    </param>
                </schemaRegistrations>
            </graphql>
        </authoring>
    </sitecore>
</configuration>

Let’s create in the root of the project a SchemaRegistration class. It should inherit from ISchemaRegistration interface. We need to implement three get-only properties: QueryExtensionTypes, MutationExtensionTypes and SubscriptionExtensionTypes. Since we have one query to register, we need to add it to QueryExtensionTypes collection in class constructor and that’s it.

using Sitecore.GraphQL.Core;
using SitecoreGroove.Feature.AuthoringAndManagementApi.Queries;
using System;
using System.Collections.Generic;

namespace SitecoreGroove.Feature.AuthoringAndManagementApi
{
    public class SchemaRegistration : ISchemaRegistration
    {
        private readonly HashSet<Type> _queryExtensionTypes = new HashSet<Type>();
        private readonly HashSet<Type> _mutationExtensionTypes = new HashSet<Type>();
        private readonly HashSet<Type> _subscriptionExtensionTypes = new HashSet<Type>();

        public IReadOnlyCollection<Type> QueryExtensionTypes => _queryExtensionTypes;
        public IReadOnlyCollection<Type> MutationExtensionTypes => _mutationExtensionTypes;
        public IReadOnlyCollection<Type> SubscriptionExtensionTypes => _subscriptionExtensionTypes;

        public SchemaRegistration()
        {
            _queryExtensionTypes.Add(typeof(ReleatedItems));
        }
    }
}

Testing New Query in GraphQL Playground

In order to test new schema in GraphQL Playground, make sure it is enabled and authorization header is set with bearer token. I’ve described how to do that in one of my Blazor Workbox series article - Blazor Workbox - Radzen DataGrid and GraphQL Authoring API.

Now, when we expand DOCS bookmark and search for relatedItem we should see related items query schema. Thanks to usage of [GraphQLDescription] attribute, it will be annotated with proper documentation.

Let’s test finally the related items query. Enter below query in the Playground query window.

     query RelatedItems(
       $itemUri: String!
       $filterByWorkflowState: Boolean!
       $excludeItemsWithLayout: Boolean!
       $recursive: Boolean!
     ) {
       relatedItems(
         relatedItemsQuery: {
           itemUri: $itemUri
           filterByWorkflowState: $filterByWorkflowState
           excludeItemsWithLayout: $excludeItemsWithLayout
           resursive: $recursive
         }
       ) {
         itemUri
         name
         version
         language
         path
         templateName
         updated
         updatedBy
         workflowStateId
       }
     }

We need to define the query parameters as bellow in the bottom left window.

{
    "itemUri": "sitecore://master/{CB6F6AE3-6554-41AC-8141-F12F62F4CE35}?lang=en&ver=1",
    "filterByWorkflowState": true,
    "excludeItemsWithLayout": false,
    "recursive": true
}

After firing the query we should see the result in the right window.

Summary

Extending Sitecore Authoring and Management GraphQL API is rather straightforward. Here are some takeaways:

  • Although almost all classes from Sitecore out-off-the-box schema are marked as internal, there is nothing that would prevent from defining a custom schema registration and creation of own custom models.

  • Thanks to <schemaRegistrations> config there is flexibility in terms of management of custom schema plugins.

  • HotChocolate framework provides handy class and method attributes that helps documenting schema extension

  • Out-off-the-box GraphQL Playground helps with testing and schema validation

Related source code is available at SitecoreGroove.Feature.AuthoringAndManagementApi