Search code examples
c#iterationienumerable

Get first element from enumerable then iterate rest


I have an IEnumerable< Task< MyObject>> that effectively describes how to fetch a collection of items from cache. The MyObject element has an error message and a success object. I would like to do something like this:

List<MyObject> fetchAll(IEnumerable<Task<MyObject>> tasks)
{
    var (firstResponse, restOfEnumerable) = await tasks.DoSomethingToGetFirstResonseAndRestOfEnumerableAsync().ConfigureAwait(false);
    if(!firstResponse.IsSuccess)
    {
        return null;
    }

    List<MyObject> ret = new List<MyObject>(firstResponse.Result);
    ret.AddRange(await Task.WhenAll(restOfEnumerable).ConfigureAwait(false));
    return ret;
}

I know I could do this by calling ToList() on the enumerable and doing this

List<MyObject> fetchAll(List<Task<MyObject>> tasks)
    {
        var firstResponse = await tasks[0].ConfigureAwait(false);
        if (!firstResponse.IsSuccess)
        {
            return null;
        }
    
        List<MyObject> ret = new List<MyObject>(firstResponse.Result);
        ret.AddRange(await Task.WhenAll(tasks.Skip(1)).ConfigureAwait(false));
        return ret;
    }

but I'm trying to not iterate the IEnumerable fully just to test the first item.

I suppose I could also drop down and get the enumerator myself like this

List<MyObject> fetchAll(IEnumerable<Task<MyObject>> tasks)
{
    var ret = new List<MyObject>();
    using (var enumerator = tasks.GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            var firstResult = await enumerator.Current.ConfigureAwait(false);
            if (!firstResponse.IsSuccess)
            {
                return null;
            }
            ret.Add(firstResponse.Result);
        }

        List<Task> restOfTasks = new List<Task>();
        while (enumerator.MoveNext())
        {
            restOfTasks.Add(enumerator.Current);
        }

        ret.AddRange(await Task.WhenAll(restOfTasks).ConfigureAwait(false));
    }
}

but I'm hoping there is something built in that I'm overlooking.


Solution

  • As mentioned in the question, I do not want to fully iterate the enumerable to test the first element and I do not want to cause a multiple enumeration of the enumerable (it may not always be possible to enumerate multiple times or it could be very expensive).

    This is what I ended up doing - seems you have to drop down to the enumerator, there's no way to pass around a partially enumerated enumerable that I could figure out.

    List<MyObject> fetchAll(IEnumerable<Task<MyObject>> tasks)
    {
        var ret = new List<MyObject>();
        using (var enumerator = tasks.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                var firstResult = await enumerator.Current.ConfigureAwait(false);
                if (!firstResponse.IsSuccess)
                {
                    return ret;
                }
                ret.Add(firstResponse.Result);
    
                // if there are more, then get all the tasks and use Task.WhenAll to start them all at once and wait for them all to finish
                if (enumerator.MoveNext())
                {
                    // also, we only create this task list if there are more
                    var restOfTasks = new List<Task<MyObject>>();
                    restOfTasks.Add(enumerator.Current);
                    while (enumerator.MoveNext())
                    {
                        restOfTasks.Add(enumerator.Current);
                    }
                    results.AddRange(await Task.WhenAll(restOfTasks).ConfigureAwait(false));
                }
            }
        }
        return ret;
    }