2

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?

gregdev
  • 31
  • 5
  • 3
    yield return is intended only for `IEnumerable` or `IAsyncEnumerable` return types.Try to rewrite to `IAsyncEnumerable` – Ryan Jul 14 '23 at 15:35
  • Thanks Ryan. Can you see any issue with my first implementation even though it compiles? – gregdev Jul 14 '23 at 15:40
  • 2
    Depends on what you mean by "issue". The result will be resolved lazily, so it's possible you could get unexpected results. For example, if you call `GetResultsFromQueryExecutionId` and then delete the CSV file before iterating over the results, you'll get an error when you go to consume the results. – StriplingWarrior Jul 14 '23 at 15:43
  • 3
    I would check it to be sure, by your stream to data source is disposed immediately after you get IEnumerable from it. Ie data source is always lost. I would rewrite `GetResultsFromQueryExecutionId` by passing stream as parameter in order to control that data source existence. – Ryan Jul 14 '23 at 15:49
  • Ok thanks, I will check whether I can still GetResultsFromQueryExecutionId with my compiled implementation. (I think IAsyncEnumerable is what I really require, I was just surpised I could not combine the code, I think this is a special case as c# would normally allow such things with other types - maybe all other types....) – gregdev Jul 14 '23 at 15:58

2 Answers2

3

As written in the specification:

The yield statement is used in an iterator block (§13.3) to yield a value to the enumerator object (§15.14.5) or enumerable object (§15.14.6) of an iterator or to signal the end of the iteration.

Currently as far as I know compiler supports returning the following types from methods using yield return:

IEnumerable
IEnumerator
IEnumerable<T>
IEnumerator<T>
IAsyncEnumerable<T>
IAsyncEnumerator<T>

And Task<IEnumerable<T>> is not one of them, so you are limited to either using approach with two methods or using IAsyncEnumerable/IAsyncEnumerator:

private async IAsyncEnumerable<T> GetResultsFromResponseStream<T>(Stream csvResponseStream)
{
    await using var csvResponseStream = ... 
    // the iterator block
}

Though personally I would use the first one (you can turn the method into a local function for convenience).

Read more:

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
1

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)
{
    ...
}
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • 1
    _"You can only return IAsyncEnumerable<> or IEnumerable<>"_ - not completely true actually =) – Guru Stron Jul 14 '23 at 15:54
  • Thank you @StriplingWarrior. I agree that returning `IAsyncEnumerable` is what I really require, however I was surprised I could not combine the two methods in my work around. I suppose "yield return is a very special construct" covers it. – gregdev Jul 14 '23 at 15:56
  • 1
    Not a fan of using `await Task.Yield()` for the purpose of offloading. My arguments are in the comments of [this answer](https://stackoverflow.com/questions/74558319/async-function-not-launching-asynchronously/74558485#74558485 "Async function not launching asynchronously") by Marc Gravell. – Theodor Zoulias Jul 14 '23 at 16:17
  • 1
    @GuruStron, reading specs is completely okay for accurate reasoning, but semantically only those *Enumerable types matter. – Ryan Jul 14 '23 at 16:22
  • @TheodorZoulias: That wasn't the point. I was simplifying so I could use code that compiles while using async patterns, without having to mock out all the methods that the OP is using. – StriplingWarrior Jul 14 '23 at 16:23
  • @GuruStron: Yeah, go and get technical on me. :-) I updated the phrasing with a caveat. If it's still inaccurate, feel free to edit my answer. – StriplingWarrior Jul 14 '23 at 16:25
  • For me any answer that promotes the `await Task.Yield()` as a mechanism for offloading, is downvotable. In case the intention is to simulate asynchronous work, I would suggest `await Task.Delay(1000); // Simulate asynchronous work`. – Theodor Zoulias Jul 14 '23 at 16:27
  • @TheodorZoulias: I didn't feel it's promoting the use of `await Task.Yield()` for offloading, any more than your suggestion promotes the use of unnecessary `Task.Delay`s. If it makes you feel better you can add a `// Simulate asynchronous work` comment, but I frankly think you're being a little hard-nosed about it. – StriplingWarrior Jul 14 '23 at 16:32
  • 2
    @Ryan my comment was not meant to be a serious one, next time will add more smiles to similar ones (if any) =) – Guru Stron Jul 15 '23 at 16:33