Search code examples
c#.net-coreasync-awaitienumerable

Returning IEnumerable<T> from yield in C#


I am able to compile

public async Task<IEnumerable<T>> GetResultsFromQueryExecutionId<T>(string queryExecutionId)
{
     await using var csvResponseStream = await transferUtility.OpenStreamAsync("bucket", "blah.csv");
     return GetResultsFromResponseStream<T>(csvResponseStream);
}

private IEnumerable<T> GetResultsFromResponseStream<T>(Stream csvResponseStream)
{
     using var streamReader = new StreamReader(csvResponseStream);
     using var csvReader = new CsvReader(streamReader, csvConfiguration);
     foreach (var record in csvReader.GetRecords<T>())
     {
          yield return record;
     }
}

but if I try to remove the private method and run the code in one method I get a compile error "The body of 'GetResultsFromQueryExecutionId' cannot be an iterator block because 'System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable>' is not an async iterator interface type"

I tried

public async Task<IEnumerable<T>> GetResultsFromQueryExecutionId<T>(string queryExecutionId, string logPrefix = null)
{
     await using var csvResponseStream = await transferUtility.OpenStreamAsync("bucket", "blah.csv");
     using var streamReader = new StreamReader(csvResponseStream);
     using var csvReader = new CsvReader(streamReader, csvConfiguration);
     foreach (var record in csvReader.GetRecords<T>())
     {
          yield return record;
     }
}

I was expecting the code to be equivalent. Is anyone able to explain why the compiler will not allow me to combine this into one method please?


Solution

  • yield return is a very special construct that forces the method it's in to get compiled in a completely different mode. You can only return IAsyncEnumerable<> or IEnumerable<> (or types with similar methods on them) from methods that use it, so if you want to return something different you'll need to break the code into separate methods as you have.

    That said, you may want to look at a couple of other options. You can use a Local Function that's nested inside of your outer function, to help keep things grouped together.

    public async Task<IEnumerable<T>> GetResultsFromQueryExecutionId<T>(string queryExecutionId)
    {
        await Task.Yield();
        return GetResultsFromResponseStream();
        IEnumerable<T> GetResultsFromResponseStream()
        {
            foreach (var record in new T[0] )
            {
                yield return record;
            }
        }
    }
    

    It might be appropriate to return an IAsyncEnumerable<T> instead:

    public async IAsyncEnumerable<T> GetResultsFromQueryExecutionId<T>(string queryExecutionId)
    {
        await Task.Yield();
        foreach (var record in new T[0] )
        {
            yield return record;
        }
    }
    

    This would change the way people use your method.

    // Before
    var results = await GetResultsFromQueryExecutionId<int>("foo");
    foreach(var result in results)
    {
        ...
    }
    // After 
    var results = GetResultsFromQueryExecutionId<int>("foo");
    await foreach (var result in results)
    {
        ...
    }