Категории Geta — отличный пакет. Это делает работу с Таксономией приятной и интуитивно понятной. И это дает огромную гибкость, поскольку категории могут содержать все, что необходимо. Это также помогает с фильтрацией поиска. Однако есть небольшая проблема, которая возникает при работе над безголовой архитектурой с Optimizely Graph в качестве центральной точки. Категории Geta не индексируются в Graph, а это означает, что схема не содержит созданных типов категорий. Самое главное, что при наличии ссылки на контент категории его расширенный объект имеет значение null. Обычно он содержит все данные категории, как и для других типов контента. К сожалению, не существует простого переключателя или параметра конфигурации для добавления типа категории в регистр индексации.
Есть проблема, которую необходимо решить. Интерфейсному приложению по-прежнему необходимо знать название категории или какие-либо важные данные. Каковы потенциальные решения?
ОТДЫХ API
График по-прежнему содержит самую важную часть данных о категории — ссылку на ее содержимое. Благодаря этому FE может получить необходимую информацию от специально подготовленной конечной точки API, которая будет возвращать информацию о категории на основе идентификатора. Или он может возвращать полный набор данных обо всех доступных категориях, которые приложение FE хранит локально. Однако это кажется ошеломляющим. Дополнительный HTTP-вызов только для получения имени категории? Выглядит не очень хорошо…
Свойство пользовательского графика
Существует способ обогатить данные Graph дополнительными свойствами для определенных типов контента. Я позаимствовал этот подход из кода Optimizely Commerce, который использует его для добавления такой информации, как цены на рынке или данные о запасах.
По сути, пользовательское свойство должно реализовывать ITypedContentApiModelProperty интерфейс (но регистрируясь в DI как IContentApiModelProperty). Легко заметить, что каждое свойство должно содержать дублированный код, например, для поиска поддерживаемых типов. В этом случае полезно сначала подготовить базовый класс, содержащий все общие функции.
internal abstract class CustomGraphPropertyBase<TGraphModel> : ITypedContentApiModelProperty where TGraphModel : notnull
{
private readonly ContentTypeModelRepository _contentTypeModelRepository;
private readonly Lazy> _supportedTypes;
protected IContentLoader ContentLoader { get; }
protected CustomGraphPropertyBase(ContentTypeModelRepository contentTypeModelRepository, IContentLoader contentLoader)
{
_contentTypeModelRepository = contentTypeModelRepository;
ContentLoader = contentLoader;
_supportedTypes = new Lazy>(ResolveSupportedTypes);
}
public Type[] ContentTypes => _supportedTypes.Value.ToArray();
public object GetValue(ContentApiModel contentApiModel)
{
var guidValue = contentApiModel.ContentLink?.GuidValue;
if (!guidValue.HasValue)
return NoValue;
try
{
var content = ContentLoader.Get(guidValue.Value, GetLanguage(contentApiModel.Language));
return GetValue(content);
}
catch
{
return NoValue;
}
}
public abstract string Name { get; }
protected abstract IEnumerable GetSupportedTypes() ;
protected abstract TGraphModel GetValue(IContentData content);
protected abstract TGraphModel NoValue { get; }
private HashSet ResolveSupportedTypes()
{
var typeSet = new HashSet();
var supportedTypes = GetSupportedTypes().ToList();
foreach (var contentTypeModel in _contentTypeModelRepository.List())
{
var modelType = contentTypeModel.ModelType;
if (supportedTypes.Any(x => x.IsAssignableFrom(modelType)))
{
typeSet.Add(modelType);
}
}
return typeSet;
}
private static CultureInfo GetLanguage(LanguageModel languageModel)
{
var langName = languageModel.Name;
if (string.IsNullOrWhiteSpace(langName))
{
return CultureInfo.InvariantCulture;
}
try
{
return CultureInfo.GetCultureInfo(langName);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}
Имея это, можно создавать определенные свойства. Например, может быть специальный интерфейс, IHasТаксономические категориикоторый отмечает контент и заставляет его содержать необходимые свойства категории. Затем пользовательское свойство можно зарегистрировать для реализаций интерфейса, которые добавляют новое поле графика — ТаксономическиеКатегорииРасширенныйгде можно найти все необходимые данные, такие как имена и описания.
internal class TaxonomicCategoriesGraphProperty : CustomGraphPropertyBase<List<CategoryGraphModel>>
{
private readonly ICategoryContentLoader _categoryLoader;
public TaxonomicCategoriesGraphProperty(
ContentTypeModelRepository contentTypeModelRepository,
IContentLoader contentLoader,
ICategoryContentLoader categoryLoader) : base(contentTypeModelRepository, contentLoader)
{
_categoryLoader = categoryLoader;
}
public override string Name => "TaxonomicCategoriesExpanded";
protected override IEnumerable<Type> GetSupportedTypes()
{
yield return typeof(IHasTaxonomicCategories);
}
protected override List<CategoryGraphModel> GetValue(IContentData content)
{
if (content is not IHasTaxonomicCategories hasCategories)
{
return NoValue;
}
return hasCategories
.TaxonomicCategories
?.Select(x => _categoryLoader.Get<CategoryData>(x))
.Select(x => new CategoryGraphModel(x.Name))
.ToList() ?? [];
}
protected override List<CategoryGraphModel> NoValue => [];
Это работает довольно хорошо, но есть существенный недостаток. Любое изменение категории, например обновление имени, не будет автоматически заполняться во всех ТаксономическиеКатегорииРасширенный поля. Чтобы увидеть это обновление, все экземпляры контента, содержащие это поле, должны быть переиндексированы.
Это также можно исправить, подключившись к соответствующим событиям контента, найдя зависимые экземпляры контента и запустив для них переиндексацию Graph. Я пытался избежать этого потока, но это не всегда возможно. Впрочем, это тема для другого поста.

Регистрация типа контента категории
Есть еще один способ, который я обнаружил недавно после серьезной отладки. Можно зарегистрировать новый тип контента, что вкратце означает, что все типы категорий будут добавлены в схему Graph, а расширенный объект будет содержать нужные данные. Он будет работать так же, как и для других блоков, страниц и носителей. Но есть БОЛЬШОЙ КРАСНЫЙ флаг здесь. Требуется зарегистрировать код для сервисов из внутренних пространств имен Optimizely.
При этом давайте углубимся в технические детали. Логика разрешения графа берет все доступные типы контента из репозитория типов контента и проверяет, разрешено ли индексирование базового типа. Первая проблема здесь заключается в том, что категории Geta не имеют базового типа, доступного в этих данных — он просто равен нулю. Итак, первый шаг — это зарегистрировать его. Подходя к этому аналогично тому, как коммерция регистрирует базовые типы для коммерческих типов, достаточно добавить новый элемент структуры и зарегистрировать его у провайдера. Он просто отображает наши новые CustomContentTypeBase структура Геты КатегорияДанные модель.
using EPiServer.DataAbstraction.RuntimeModel;
using Geta.Optimizely.Categories;
internal static class CustomContentTypeBase
{
internal static readonly ContentTypeBase Category = new(nameof(Category));
}
internal class CustomContentTypeBaseProvider : IContentTypeBaseProvider
{
private static readonly Dictionary ContentTypeBasesMapping = new()
{
{
CustomContentTypeBase.Category,
typeof(CategoryData)
}
};
public IEnumerable ContentTypeBases => ContentTypeBasesMapping.Keys;
public Type? Resolve(ContentTypeBase contentTypeBase) =>
ContentTypeBasesMapping.TryGetValue(contentTypeBase, out var type) ? type : null;
}
Однако этого недостаточно. Теперь необходимо сообщить Graph, что этот новый базовый тип можно индексировать. И вот тут-то внутренний код надо немного выровнять, потому что кастомная реализация для Optimizely.ContentGraph.Cms.NetCore.Internal необходимо добавить. Он ничего не делает, просто добавляет новый элемент в список.
internal class CustomContentSerializer : ContentSerializer
{
public CustomContentSerializer(
IContentConverterResolver contentConverterResolver,
ContentApiOptions contentApiOptions,
IContentTypeRepository contentTypeRepository,
IContentGraphConverterContextFactory cgConverterContextFactory,
IEnumerable contentApiModelFilters,
ISiteDefinitionResolver siteDefinitionResolver,
ContentGraphContextAccessor contentGraphContextAccessor,
IOptions queryOptions,
ConventionRepository? conventionRepository = null )
: base(
contentConverterResolver,
contentApiOptions,
contentTypeRepository,
cgConverterContextFactory,
contentApiModelFilters,
siteDefinitionResolver,
contentGraphContextAccessor,
queryOptions,
conventionRepository)
{
}
public override string[] SupportedContentBaseTypes
{
get
{
var supportedTypes = base.SupportedContentBaseTypes.ToList();
supportedTypes.Add(CustomContentTypeBase.Category.ToString());
return supportedTypes.ToArray();
}
}
}
Это не большая часть пользовательской логики. Просто добавляем наш новый CustomContentTypeBase в словарь поддерживаемых типов. Но все же это зависит от множества сервисов, внедренных в конструктор, которые легко изменить. И я уже сталкивался с этим после обновления пакета Graph — приходилось исправлять ошибки компиляции…
После этого последним шагом будет регистрация всего в контейнере DI. После этого категории можно увидеть в проводнике схемы графа.

Развернутый объект содержит сведения о категории. Что здесь также важно — он будет обновляться каждый раз при обновлении актуальной категории.

Краткое содержание
Переход к безголовой архитектуре сопряжен с новыми проблемами. Один из них — полностью использовать силу категорий Гета. В некоторых случаях необходимы обходные пути, подобные описанным выше. Я искренне верю, что самый оптимальный способ — регистрация нового типа контента для индексации — будет полностью поддерживаться в ближайшем будущем.
Продолжение темы

