Skip to content

CSHARP-3458: Extend IAsyncCursor and IAsyncCursorSource to support IAsyncEnumerable #1708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

adelinowona
Copy link
Contributor

@adelinowona adelinowona commented Jun 12, 2025

No description provided.

@adelinowona adelinowona requested a review from a team as a code owner June 12, 2025 16:05
@adelinowona adelinowona requested review from sanych-sun and removed request for a team June 12, 2025 16:05
@adelinowona adelinowona requested review from rstam and BorisDog and removed request for sanych-sun June 12, 2025 16:06
Copy link
Contributor

@BorisDog BorisDog left a comment

Choose a reason for hiding this comment

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

LGTM!

@sanych-sun
Copy link
Member

As far as I know IAsyncEnumerator implements IAsyncDisposable interface to perform async clean up. I know that current implementation of AsyncCursor do not implements IAsyncDisposable, but there is an async method for clean up (see CloseIfNotAlreadyClosedAsync). Should we add support of IAsyncDisposable to AsyncCursor (probably in separate ticket/PR)?

{
// private fields
private readonly CancellationToken _cancellationToken;
private readonly IAsyncCursorSource<TDocument> _source;

// constructors
public AsyncCursorSourceEnumerableAdapter(IAsyncCursorSource<TDocument> source)
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest to call another constructor here, like:
: this(source, CancellationToken.None)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

Choose a reason for hiding this comment

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

I would not have added this constructor.

It makes it too easy for the caller to forget to pass in the cancellationToken.

Let the caller provide CancellationToken.None if it needs to.

@adelinowona adelinowona requested a review from sanych-sun June 26, 2025 15:31
{
private readonly CancellationToken _cancellationToken;
private readonly IAsyncCursor<TDocument> _cursor;
private bool _hasBeenEnumerated;

public AsyncCursorEnumerableOneTimeAdapter(IAsyncCursor<TDocument> cursor)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not have added this constructor.

It makes it too easy for the caller to forget to pass in the cancellationToken.

Let the caller provide CancellationToken.None if it needs to.

{
// private fields
private readonly CancellationToken _cancellationToken;
private readonly IAsyncCursorSource<TDocument> _source;

// constructors
public AsyncCursorSourceEnumerableAdapter(IAsyncCursorSource<TDocument> source)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not have added this constructor.

It makes it too easy for the caller to forget to pass in the cancellationToken.

Let the caller provide CancellationToken.None if it needs to.

@@ -72,6 +73,12 @@ public void Dispose()
}
}

public ValueTask DisposeAsync()
{
Dispose();
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add a comment like:

 Dispose(); // TODO: implement true async disposal
public ValueTask DisposeAsync()
{
Dispose();
return default;
Copy link
Contributor

Choose a reason for hiding this comment

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

My first reaction was that default was a bug, that we should be returning ValueTask.CompletedTask.

It took me some research to figure out why you were returning default.

I suggest the following:

#if NET6_0_OR_GREATER
            return ValueTask.CompletedTask;
#else
            return default; // prior to NET6_0 you have to fake ValueTask.CompletedTask using default
#endif

It's a little more verbose, but it documents why we sometimes have to use default instead of ValueTask.CompletedTask, and when we remove support for netstandard2.1 this will get cleaned up.

Copy link
Member

Choose a reason for hiding this comment

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

It's not a bug definitely. This is documented behavior:

An instance created with the parameterless constructor or by the default(ValueTask) syntax (a zero-initialized structure) represents a synchronously, successfully completed operation.

https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-9.0#remarks

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't say it was a bug. I meant it "looked" like a bug.

I'm saying that we should use ValueTask.CompletedTask when available.

Copy link
Member

Choose a reason for hiding this comment

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

Yep, but it's technically the same.
image
ValueTask.CompletedTask is more readable though.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't say it wasn't technically the same.

I'm saying that we should use ValueTask.CompletedTask when available. It's more readable than default (who wants to go hunt down the documentation to figure out what default means).

public AsyncCursorSourceEnumerableAdapter(IAsyncCursorSource<TDocument> source, CancellationToken cancellationToken)
{
_source = Ensure.IsNotNull(source, nameof(source));
_cancellationToken = cancellationToken;
}

public IAsyncEnumerator<TDocument> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
var cursor = _source.ToCursor(cancellationToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

Calling _source.ToCursor is a BLOCKING call that executes the query.

Presumable the caller did not want to execute the query synchronously.

Can we defer query execution until asyncEnumerator.MoveNextAsync() is called?

Thoughts?

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM, but we probably need to create a ticket for proper IAsyncDisposable implementation.

public ValueTask DisposeAsync()
{
Dispose();
return default;
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't say it was a bug. I meant it "looked" like a bug.

I'm saying that we should use ValueTask.CompletedTask when available.

public ValueTask DisposeAsync()
{
Dispose();
return default;
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't say it wasn't technically the same.

I'm saying that we should use ValueTask.CompletedTask when available. It's more readable than default (who wants to go hunt down the documentation to figure out what default means).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
4 participants