Creating Custom GraphQL Schemas

Creating custom schemas for your GraphQL query that will be used only in Master, Web or Core GraphQL APIs.

May 29, 2024

By Arsalaan Pathan

Brief Insight Into Custom GraphQL Schema

Why to create a custom schema? The simple answer to this would be, If you have any custom requirement for specific fields or a specific output from a graphql query but the out of the box queries cannot fulfill it.

Creating custom queries requires the following steps.

  1. Create a C# class which implements the SchemaProviderBase class.
  2. Create a C# class which implements the RootFieldType class.
  3. Create a C# class which implements the ObjectGraphType class.
  4. Register this class on GraphQL endpoint using a config patch file.

Let’s start with the first step. Here we will create a custom item query which will work similar to the item query provided out of the box by sitecore. Have a look at the code below.

using System;
using System.Collections.Generic;
using GraphQL.Types;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Globalization;
using Sitecore.Services.GraphQL.Schemas;
using Sitecore.Services.GraphQL.Content.GraphTypes;
using Sitecore.Services.GraphQL.GraphTypes;
using System.Linq;
using Newtonsoft.Json.Linq;

namespace MyLocal.Foundation.Site.GraphQL.Schemas
{
    /// <summary>
    /// Sample of making your own schema provider
    /// This sample enables you to query on the current context user
    /// </summary>
    public class CustomItemQueryProvider : SchemaProviderBase
    {
        public override IEnumerable<FieldType> CreateRootQueries()
        {
            yield return new CustomItemQuery();
        }

        /// <summary>
        /// Teaches GraphQL how to resolve the `CustomItemQuery` root field.
        ///
        /// RootFieldType<CustomItemGraphType, Item> means this root field maps a `Item` domain object into the `CustomItemGraphType` graph type object.
        /// </summary>
        protected class CustomItemQuery : RootFieldType<CustomItemGraphType, Item>
        {
            public CustomItemQuery() : base(name: "customItemQuery", description: "Gets the item data")
            {
                // Define the arguments which will be used in the query
                QueryArguments qA = (base.Arguments = new QueryArguments(new QueryArgument<StringGraphType>
                {
                    Name = "itemPath",
                    Description = "The item path or id"
                }, new QueryArgument<StringGraphType>
                {
                    Name = "language",
                    Description = "The item language to request"
                }));
            }

            protected override Item Resolve(ResolveFieldContext context)
            {
                // Fetch the arguments value from the query
                string lang = context.GetArgument<string>("language");
                Language result = null;
                if (lang != null && !Language.TryParse(lang, out result))
                {
                    throw new InvalidOperationException("Unable to parse requested language.");
                }
                if (result != null)
                {
                    Sitecore.Context.Language = result;
                }
                else
                {
                    result = Sitecore.Context.Language ?? LanguageManager.DefaultLanguage;
                }

                string itemPath = context.GetArgument<string>("itemPath");
                using (new LanguageSwitcher(result))
                {
                    if (itemPath == null)
                    {
                        return Sitecore.Context.Database.GetRootItem(result);
                    }
                    if (!IdHelper.TryResolveItem(Sitecore.Context.Database, itemPath, result, -1, out var item))
                    {
                        return null;
                    }
                    if (item == null || item.Versions.Count == 0)
                    {
                        return null;
                    }

                    return item;
                }
                // this is the object the resolver maps onto the graph type
                // (see CustomItemGraphType below). This is your own domain object, not GraphQL-specific.
                //return Sitecore.Context.Item;
            }
        }

        // because this graph type is referred to by the return type in the FieldType above, it is automatically
        // registered with the schema. For implied types (e.g. interface implementations) you need to override CreateGraphTypes() and
        // manually tell the schema they exist (because no graph type directly refers to those types)
        protected class CustomItemGraphType : ObjectGraphType<Item>
        {
            public CustomItemGraphType()
            {
                // graph type names must be unique within a schema, so if defining a multiple-schema-provider
                // endpoint, ensure that you don't have name collisions between schema providers.
                Name = "CustomItemQuery";

                Field<NonNullGraphType<StringGraphType>>("id", resolve: context => context.Source.ID);
                Field<NonNullGraphType<StringGraphType>>("itemname", resolve: context => context.Source.Name);
                Field<NonNullGraphType<StringGraphType>>("templateid", resolve: context => context.Source.TemplateID);
                Field<NonNullGraphType<StringGraphType>>("templatename", resolve: context => context.Source.TemplateName);
                Field<NonNullGraphType<JsonGraphType>>("fields", resolve: context =>
                {
                    return GetAllFeildsJSON(context);
                });

                // note that graph types can resolve other graph types; for example
                // it would be possible to add a `lockedItems` field here that would
                // resolve to an `Item[]` and map it onto `ListGraphType<ItemInterfaceGraphType>`
            }

            private JObject GetAllFeildsJSON(ResolveFieldContext<Item> context)
            {
                JObject fields = new JObject();

                foreach (Sitecore.Data.Fields.Field field in context.Source.Fields.OrderBy(s => s.Sortorder))
                {
                    if (field.Name.StartsWith("__"))
                    {
                        continue;
                    }
                    // Resolve other fields such as multilist, droplink, droplist etc as per requirement as done below.
                    if (String.Compare(field.Type, "General Link", true) == 0)
                    {
                        Sitecore.Data.Fields.LinkField link = field;
                        JObject linkValue = new JObject()
                        {
                            ["text"] = link.Text,
                            ["linktype"] = link.LinkType,
                            ["anchor"] = link.Anchor,
                            ["href"] = link.GetFriendlyUrl()
                        };

                        fields[field.Name] = linkValue;
                    }
                    else
                    {
                        JObject fieldValue = new JObject()
                        {
                            ["value"] = field.Value,
                        };
                        fields[field.Name] = fieldValue;
                    }
                }

                return fields;
            }
        }
    }
}

Note – The example above uses nested classes but you can separate them out as per your need.

As you can see, I have created a C# class named CustomItemQueryProvider which inherits the SchemaProviderBase class. This is an abstract class which provides you an abstract function CreateRootQueries(). You will have to override this function to create your custom query. In the function definition we return the object of CustomItemQuery class.

The CustomItemQuery class inherits the RootFieldType class. The RootFieldType class is responsible for mapping the sitecore Item object to the graph type object, in this case the CustomItemGraphType class.

protected class CustomItemQuery : RootFieldType<CustomItemGraphType, Item>

The RootFieldType constructor takes 2 arguments, the name of the query and the description of the query. This name and description is used to create a new schema in experience edge.

public CustomItemQuery() : base(name: "customItemQuery",description: "Gets the item data")

For this query we will use 2 arguments, itemPath which will be an id of datasource item in sitecore and language which will be used to fetch the datasource item fields in this specific language. The parameters need to be initialized in the constructor of CustomItemQuery class.

QueryArguments qA = (base.Arguments = new QueryArguments(new QueryArgument<StringGraphType>
                {
                    Name = "itemPath",
                    Description = "The item path or id"
                }, new QueryArgument<StringGraphType>
                {
                    Name = "language",
                    Description = "The item language to request"
                }));

While initializing the argument we need to define the name, description and the type of argument. Here, both of the arguments are StringGraphType. Once the arguments are initialized, the constructor of CustomItemGraphType class is called.

The CustomItemGraphType inherits from the ObjectGraphType class which needs a GraphType object to be passed along. In our case it is Sitecore.Data.Items.Item.

protected class CustomItemGraphType : ObjectGraphType<Item>

The ObjectGraphType class provides with a function called as Field which is useful in creating fields for the query and resolving the values of fields.

Field<NonNullGraphType<StringGraphType>>("id", resolve: context => context.Source.ID);

The resolve function used in the argument of Field function is responsible for resolving the field response based on the GraphType object. For E.g. – currently we have the GraphType as Sitecore.Data.Items.Item so we will be able to use all the fields available in the item such as templateId, templateName, id etc. If the GraphType is Sitecore.Data.Fields then will be able to use fields such as Value, hasValue, Style, SortOrder etc.

// If GraphType is Item
Field<NonNullGraphType<StringGraphType>>("templatename", resolve: context =>context.Source.TemplateName);

// If GraphType is Field
Field<NonNullGraphType<StringGraphType>>("sortOrder", resolve: context =>context.Source.Sortorder);

Note – When the code is deployed on your environment the CustomItemGraphType constructor is called to register the fields of the query along with the schema. This initial call to constructor will only register the fields and not resolve them. The resolution of the fields will happen when the query is run and the constructor is called again.

Here, the basic structure of the query is ready. Now we need to resolve the values for those fields. For this will take a look at the Resolve function that is used inside the CustomItemQuery class. This function will execute when the query in run in experience edge.

protected override ItemResolve(ResolveFieldContextcontext)

The Resolve function needs to be overridden here as it is part of RootFieldType abstract class. This function takes one argument which is an object of ResolveFieldContext class. This object provides you with the context values of the query executed in experience edge such as the arguments itemPath and language. With the help of this object we fetch the item id and language, And as we are creating an item query we return an item as a result.

Once the resolve function is executed, the constructor of CustomItemGraphType is called again, this time to resolve the values of the fields and return it back.

The last step would be to register your query. For this will have to create a patch config file similar to below code.

<?xml version="1.0" encoding="utf-8" ?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>
        <api>
            <GraphQL>
                <endpoints>
                    <master url="/sitecore/api/graph/items/master" type="Sitecore.Services.GraphQL.Hosting.GraphQLEndpoint, Sitecore.Services.GraphQL.NetFxHost">
                        <schema hint="list:AddSchemaProvider">
                            <whoDat type="MyLocal.Foundation.Site.GraphQL.Schemas.CustomItemQueryProvider, MyLocal.Foundation.Site" />
                        </schema>
                    </master>
                </endpoints>
            </GraphQL>
        </api>
    </sitecore>
</configuration>

This is the patch file for Master API, you can add something similar for Web and Core API’s. Make sure to change the API paths in the config. Once deployed your query is ready for use.

Check below screen shot which shows the schema and description of the query in experience edge.

Screenshot of a GraphQL API documentation interface showing queries, mutations, and schema details.



Picture of Fishtank employee Arsalaan Pathan

Arsalaan Pathan

Sitecore Developer

Arsalaan is not just a web developer; he's a certified expert in Sitecore and Content Hub development, bringing a unique blend of technical prowess and creative vision to every project. Since diving into the world of web development in 2016, he has continually honed his skills, initially on .NET platforms before transitioning seamlessly to Sitecore in 2020. With a passion for crafting dynamic and engaging digital experiences, Arsalaan is driven by the challenge of pushing boundaries and delivering innovative solutions that captivate audiences and drive results.

In his free time, he often savors the thrill of spontaneous road trips, exploring new destinations and soaking in the sights along the way. Alternatively, you'll find him on the soccer field, eagerly chasing after the ball, his passion for the game evident in the beads of sweat glistening on his brow. Whether traversing open roads or dominating the field, he embraces each moment with gusto and determination.