Filtering content nodes on reference property via Delivery API

I want to filter some event nodes based on region via Delivery API.

We have done similar features for categories on nodes using MTNP.

However in this specific use-case an event has a location picked using MTNP and region itself is parent node of this.

I noticed the nodes in DeliveryContentIndex already as parentId and ancestorIds fields, which could be used.

I wonder what best a best practise to handle this?

We could lookup of content from location property on event node and index a field region on event.

E.g. the filter may look something like this:

public class RegionFilter : IFilterHandler //, IContentIndexHandler
{
	private const string FilterOptionSpecifier = "region:";
	private const string IndexFieldName = "parentId";
	private const string ContentFieldName = nameof(Event.Location);

	// Querying
	public bool CanHandle(string query)
		=> query.StartsWith(FilterOptionSpecifier, StringComparison.OrdinalIgnoreCase);

	public FilterOption BuildFilterOption(string filter)
	{
		var fieldValue = filter.Substring(FilterOptionSpecifier.Length);
		var values = fieldValue.Split(',');

		return new FilterOption
		{
			FieldName = IndexFieldName,
			Values = values,
			// use exact match
			Operator = FilterOperation.Is
		};
	}

	// Indexing
	/*public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
	{
		var fieldValue = content.GetValue<string?>(ContentFieldName);
		if (fieldValue is null)
		{
			return Array.Empty<IndexFieldValue>();
		}
		var keys = fieldValue.Split(',').Select(udi => new GuidUdi(new Uri(udi)).Guid).ToArray();
		return new[]
		{
			new IndexFieldValue
			{
				FieldName = IndexFieldName,
				// index multiple values instead of one string value
				Values = keys.OfType<object>()
			}
		};
	}*/

	public IEnumerable<IndexField> GetFields() => new[]
	{
		new IndexField
		{
			FieldName = IndexFieldName,
			FieldType = FieldType.StringRaw,
			VariesByCulture = false
		}
	};
}

But since the parentId (region id), we want to search/filter is not directly on event, I guess best practise would be to index a new field on event node for picked location.

Regarding indexing I guess we can just use GetFieldValues() here and doesn’t need to use TransformingIndexValues on DeliveryApiContentIndex like this:

public void Initialize()
{
	if (!_examineManager.TryGetIndex(UmbracoIndexes.DeliveryApiContentIndexName, out IIndex index))
	{
		throw new InvalidOperationException($"No index found by name {UmbracoIndexes.DeliveryApiContentIndexName}");
	}

	index.TransformingIndexValues += UmbracoContextIndex_TransformingIndexValues;
}

I found this example from Warren, which was something similar I considered:

I tried it out, however I get an boot error when inject e.g. IUmbracoContextFactory or IPublishedContentQuery into constructor.

Not sure if this is a bug?

It seems we can inject IPublishedContentQueryAccessor for this purpose as Andy pointed out in the linked issue in CMS repository.
However for some reason it doesn’t go into this statement.

if (_publishedContentQueryAccessor.TryGetValue(out IPublishedContentQuery? publishedContentQuery))
{
  // Do stuff with publishedContentQuery.
}

My workaround for now is this instead:

HashSet<Guid> regionIds = [];

foreach (var locationKey in locationKeys)
{
    var location = cref.UmbracoContext.Content?.GetById(locationKey) as Location;
    if (location is not null && location.Parent is Region region)
    {
        regionIds.Add(region.Key);
    }
}

return
[
    new IndexFieldValue
    {
        FieldName = "region",
        // index multiple values instead of one string value
        Values = regionIds.OfType<object>()
    }
];

While it was working using content cache via UmbracoContext Kenn and Andy helped me. :raising_hands:

It didn’t work injecting IPublishedContentQuery or IPublishedContentQueryAccessor directly as these rely on HttpContext as the indexing for DeliveryContentApiIndex is running in background.

It can be access this way instead:

using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext();
using IServiceScope serviceScope = _serviceProvider.CreateScope();
IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();
var locationKeys = fieldValue.Split(',').Select(udi => new GuidUdi(new Uri(udi)).Guid).ToArray();

var locations = query?.Content(locationKeys)?.OfType<Location>() ?? [];

HashSet<Guid> regionIds = [];

foreach (var location in locations)
{
    if (location is not null && location.Parent is Region region)
    {
        regionIds.Add(region.Key);
    }
}