bjarnef
(Bjarne Fyrstenborg)
June 25, 2026, 11:09am
1
I was looking at fetching result through in Content Delivery API, but sorted random.
With some suggestion from AI:
public class RandomIndexer : IContentIndexHandler
{
internal const string FieldName = "randomOrder";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
{
var seed = content.Key.GetHashCode();
return
[
new IndexFieldValue
{
FieldName = FieldName,
Values = [Math.Abs(seed % 100000)]
}
];
}
public IEnumerable<IndexField> GetFields() =>
[
new IndexField
{
FieldName = FieldName,
FieldType = FieldType.Number,
VariesByCulture = false
}
];
}
public class RandomSort : ISortHandler
{
private const string SortOptionSpecifier = "random:";
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort[SortOptionSpecifier.Length..];
return new SortOption
{
FieldName = RandomIndexer.FieldName,
Direction = sortDirection.StartsWith("asc", StringComparison.OrdinalIgnoreCase)
? Direction.Ascending
: Direction.Descending
};
}
}
and eventually:
If you need the order to change periodically
A common pattern is to rotate the seed daily or weekly:
var daySeed = DateOnly.FromDateTime(DateTime.UtcNow).DayNumber;
var randomValue = HashCode.Combine(content.Key, daySeed);
Rebuild the index on your chosen schedule.
Howver it will most likely require to re-index daily, weekly…
If only fewer results the items can be shuffled on client-side.
Is there a better way to handle this for a larger data set, e.g. 1000 content nodes, but one wants 30 random items?
Hi @bjarnef
The AI response I get suggest to build your own endpoint that mimics the output from the Content Delivery API.
A separate controller that reuses IApiContentResponseBuilder so output matches the Delivery API shape exactly. This is the most maintainable route.
I’ve not tested this, but could something like this work for you?
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Security; // ProtectedAccess
namespace MySite.DeliveryApi;
[ApiVersion("2.0")]
[VersionedDeliveryApiRoute("content/random")]
[ApiExplorerSettings(GroupName = "Random Content")]
public class RandomContentApiController : ContentApiControllerBase
{
private readonly IApiContentQueryService _queryService;
public RandomContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IApiContentQueryService queryService)
: base(apiPublishedContentCache, apiContentResponseBuilder)
{
_queryService = queryService;
}
[HttpGet]
[MapToApiVersion("2.0")]
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Random(
string? fetch = null,
[FromQuery] string[]? filter = null,
[FromQuery] string[]? sort = null,
int take = 30,
int candidatePoolCap = 1000)
{
if (take < 1) return BadRequest("take must be at least 1.");
// ExecuteQuery returns a plain PagedModel<Guid> — no Attempt wrapper.
// It parses the fetch/filter/sort strings via the registered handlers.
PagedModel<Guid> queryResult = _queryService.ExecuteQuery(
fetch,
filter ?? Array.Empty<string>(),
sort ?? Array.Empty<string>(),
ProtectedAccess.None,
skip: 0,
take: candidatePoolCap); // bound the candidate set, don't pull int.MaxValue
var candidateKeys = queryResult.Items.ToList();
if (candidateKeys.Count == 0)
{
return Ok(Enumerable.Empty<IApiContentResponse>());
}
// Reservoir sample — avoids sorting the whole candidate set.
var sampledKeys = Reservoir(candidateKeys, Math.Min(take, candidateKeys.Count));
var responses = new List<IApiContentResponse>(sampledKeys.Count);
foreach (var key in sampledKeys)
{
// IApiPublishedContentCache (inherited as ApiPublishedContentCache).
var content = await ApiPublishedContentCache.GetByIdAsync(key);
if (content is null) continue;
// Response builder runs UNCACHED per item — keep `take` modest.
var response = ApiContentResponseBuilder.Build(content);
if (response is not null) responses.Add(response);
}
return Ok(responses);
}
private static List<T> Reservoir<T>(IReadOnlyList<T> source, int k)
{
var sample = new List<T>(source.Take(k));
for (var i = k; i < source.Count; i++)
{
var j = Random.Shared.Next(i + 1);
if (j < k) sample[j] = source[i];
}
return sample;
}
}
Rebuilding an index regularly seems the wrong solution here…
Justin
bjarnef
(Bjarne Fyrstenborg)
June 25, 2026, 4:46pm
3
Hi @justin-nevitech
From your code sample it looks like it just fetch 1000 items and from that select 30 random keys to lookup content from cache and return content items via ApiContentResponseBuilder.
It doesn’t seem it would ever fetch items from e.g. number 1001 - 5000.
Hi @bjarnef
That’s correct, but you can amend for your own requirements - either increase the limit or use a more efficient mechanism to retrieve a random number of content items depending on how many you have. I don’t know how this would perform if you had thousand of items though, and to be honest - I would question that value of returning a random selection from that many items?
Justin