To reduce the payload size of individual calls when loading up resources by ID, sometime you want to send multiple async requests in smaller batches. If we don't need to worry about local/remote resources (i.e. don't need intelligent partitioning or resource friendly approach), the easiest way is to fire off a load of tasks which consume a small batch from the superset. Here is a simple re-usable implementation:

public class BatchContentRequestor<TId, TValue>
{
    private readonly int _batchSize;
    private readonly Func<IEnumerable<TId>, Task<IEnumerable<TValue>>> _getContentAsyncFunc;

    public BatchContentRequestor(int batchSize, Func<IEnumerable<TId>, Task<IEnumerable<TValue>>> getContentAsyncFunc)
    {
        if (batchSize <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(batchSize), "Batch size must be a positive integer value.");
        }

        _batchSize = batchSize;
        _getContentAsyncFunc = getContentAsyncFunc ?? throw new ArgumentNullException(nameof(getContentAsyncFunc));
    }

    public async Task<IEnumerable<TValue>> GetContentBatchedAsync(IEnumerable<TId> allContentIds)
    {
        var allContentIdsList = allContentIds?.ToList();

        if (allContentIdsList == null || !allContentIdsList .Any())
        {
            return await _getContentAsyncFunc(allContentIdsList );
        }

        var allContentValues = new List<TValue>();

        var getBatchTasks = new List<Task<IEnumerable<TValue>>>();
        for (var batchStart = 0;
            batchStart < allContentIdsList.Count;
            batchStart += _batchSize)
        {
            var batchIds = allContentIdsList
                .Skip(batchStart)
                .Take(_batchSize)
                .ToList();

            getBatchTasks.Add(_getContentAsyncFunc(batchIds));
        }

        await Task.WhenAll(getBatchTasks).ConfigureAwait(false);

        foreach (var completedBatch in getBatchTasks)
        {
            allContentValues.AddRange(await completedBatch.ConfigureAwait(false));
        }

        return allContentValues;
    }
}
You can call it with your superset and it will automatically hit your callback function with the batches of IDs, will collect the results and return the superset of values. If the calling code passes a null or empty value this will still be passed to your callback for handling, making this a transparent proxy for the calling code. e.g.

var items = await new BatchContentRequestor<int, Item>(10, GetItemsByIdAsync).GetContentBatchedAsync(allItemIds).ConfigureAwait(false);