Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 17, 2025

Dapper added QueryUnbufferedAsync returning IAsyncEnumerable<T> in version 2.1+, eliminating the need to buffer results when using ToAsyncEnumerable().

Changes

  • Updated ListImpl: Changed to return IAsyncEnumerable<T> using Dapper's QueryUnbufferedAsync to stream results directly from the database
  • Updated ToAsyncEnumerable: Now directly returns ListImpl for true streaming (no wrapper needed)
  • Updated ListAsync: Buffers results from the async enumerable into a List<T>
  • Removed resolved TODOs: Deleted comments about waiting for Dapper IAsyncEnumerable support
  • Applied to both Query<T> and QueryIndex<T>: Same pattern applied consistently across both implementations

Before/After

Before:

async IAsyncEnumerable<T> IQuery<T>.ToAsyncEnumerable(CancellationToken cancellationToken)
{
    // TODO: Once Dapper supports IAsyncEnumerable we can replace this call by a non-buffered one
    foreach (var item in await ListImpl(cancellationToken))  // Buffers all results
    {
        yield return item;
    }
}

internal async Task<IEnumerable<T>> ListImpl(CancellationToken cancellationToken)
{
    // ... uses QueryAsync with CommandFlags.Buffered
}

After:

IAsyncEnumerable<T> IQuery<T>.ToAsyncEnumerable(CancellationToken cancellationToken)
{
    return ListImpl(cancellationToken);  // Directly returns the stream
}

internal async IAsyncEnumerable<T> ListImpl([EnumeratorCancellation] CancellationToken cancellationToken)
{
    // ... uses QueryUnbufferedAsync for streaming
}

async Task<IEnumerable<T>> IQuery<T>.ListAsync(CancellationToken cancellationToken)
{
    var results = new List<T>();
    await foreach (var item in ListImpl(cancellationToken))
    {
        results.Add(item);
    }
    return results;
}

The new implementation streams results row-by-row using connection.QueryUnbufferedAsync<T>().WithCancellation(cancellationToken), reducing memory footprint for large result sets. This eliminates code duplication and follows the original TODO guidance to "return it by default, and buffer it in ListAsync instead."

Original prompt

This section details on the original issue you should resolve

<issue_title>Use QueryUnbufferedAsync in DefaultQuery ToAsyncEnumerable</issue_title>
<issue_description>I noticed that DefaultQuery still has a TODO here about updating it when Dapper supports IAsyncEnumerable.

Since this PR was merged last year, it seems like it should support everything for this TODO to be resolved, or am I wrong with this assumption?</issue_description>

<agent_instructions>LitstImpl can now return IAsyncEnumerable. Include other required packages for TFMs that require them.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@sebastienros Maybe? Thanks for submitting a PR if you can.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@sebastienros sebastienros marked this pull request as ready for review November 17, 2025 16:21
Copilot AI changed the title [WIP] Use QueryUnbufferedAsync in DefaultQuery ToAsyncEnumerable Use QueryUnbufferedAsync for streaming IAsyncEnumerable results Nov 17, 2025
Copilot AI requested a review from sebastienros November 17, 2025 16:22
Copilot finished work on behalf of sebastienros November 17, 2025 16:22
{
// TODO: [IAsyncEnumerable] Once Dapper supports IAsyncEnumerable we can replace this call by a non-buffered one
foreach (var item in await ListImpl(cancellationToken))
await foreach (var item in ListImplAsync(cancellationToken))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Is the foreach necessary? Can't the inner result be returned directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! I've simplified it to directly return ListImpl(cancellationToken) in commit ea9a596.


#pragma warning disable CS8425 // Async-iterator member has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
async IAsyncEnumerable<T> IQuery<T>.ToAsyncEnumerable(CancellationToken cancellationToken)
#pragma warning restore CS8425 // Async-iterator member has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can this pragma be removed an fixed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in commit ea9a596! The pragma was removed by simplifying the implementation - ToAsyncEnumerable now directly returns ListImpl instead of being an async iterator.

}
}

internal async IAsyncEnumerable<T> ListImplAsync([EnumeratorCancellation] CancellationToken cancellationToken)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why does it need to be a new method? What about replacing the existing one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! I've replaced ListImpl to return IAsyncEnumerable<T> and updated ListAsync to buffer the results. This eliminates the duplicate method and follows the TODO comment's guidance. See commit ea9a596.


#pragma warning disable CS8425 // Async-iterator member has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
async IAsyncEnumerable<T> IQuery<T>.ToAsyncEnumerable(CancellationToken cancellationToken)
#pragma warning restore CS8425 // Async-iterator member has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can this pragma be removed an fixed?

- ToAsyncEnumerable now directly returns ListImpl result
- Removed pragma warning by eliminating unnecessary wrapper
- ListImpl now returns IAsyncEnumerable<T> instead of separate ListImplAsync method
- ListAsync buffers results from the async enumerable
- Applied same pattern to QueryIndex class

Co-authored-by: sebastienros <[email protected]>
Copilot AI requested a review from sebastienros November 17, 2025 16:44
Copilot finished work on behalf of sebastienros November 17, 2025 16:44
Copilot finished work on behalf of sebastienros November 17, 2025 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use QueryUnbufferedAsync in DefaultQuery ToAsyncEnumerable

2 participants