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