Your StreamRecords
method returns essentially a consuming sequence, similar in nature with the sequences returned by the methods BlockingCollection<T>.GetConsumingEnumerable
and ChannelReader<T>.ReadAllAsync
. Consuming means that when the caller enumerates the sequence, the returned elements are permanently removed from some backing storage. In the case of these two methods the backing storage is an internal ConcurrentQueue<T>
. In your case (based on this comment) the backing storage is located server-side, with some client-side code that knows what data to fetch next.
Exposing a consuming sequence comes with some challenges:
- Should the method have the word Consuming or Destructive as part of its name?
- What happens in case of cancellation? Is the sequence responsive enough when a cancellation signal arrives? Are the caller's expectations met?
- What happens if the caller abandons the enumeration prematurely, either by deliberately
break
ing or return
ing from the await foreach
loop, or unwillingly by a transient exception thrown inside the loop? Are any consumed elements in danger of being lost?
Regarding the 1st challenge you can read this GitHub issue, or even better watch this video, where the various options were debated. Spoiler alert, the Microsoft engineers settled for the seemingly innocuous ReadAllAsync
.
Regarding the 2nd challenge you can read this question, showing that the (technically justified) decision made by Microsoft regarding the ChannelReader<T>.ReadAllAsync
API, resulted in non-intuitive/unexpected behavior.
Regarding the 3rd challenge you could consider taking advantage of the mechanics of the finally
blocks inside iterator methods. Check out this answer for more details.
Because of these nuances, it might be a good idea to give additional consuming options to the callers. Something like public Task<Record[]> TakeAllNewRecords()
for example.