Skip to content
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

Add IAsyncEnumerable<SftpFile> to enumerate remote files #907

Closed

Conversation

IgorMilavec
Copy link
Collaborator

@drieseng, this is my proposal on how to gradually implement IAsyncEnumerable for asynchronously enumerating remote files.

First, the prerequisites:

  • to get IAsyncEnumerable I had to add netstandard2.1 target
  • to get IAsyncEnumerable state machine compiler support I had to raise C# version to 8.0

The latter means SSH.NET would no longer compile with VS 2017 or earlier. Is this an issue?

I propose to have two distinct methods for enumerating remote files:

  1. Task<IEnumerable<SftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken):
    This method works asynchronously, but will only complete the task after all the files have been read from the remote.
  2. IAsyncEnumerable<SftpFile> EnumerateDirectoryAsync(string path, [EnumeratorCancellation]CancellationToken cancellationToken):
    This method works asynchronously too, but will return remote files in chunks as the are received by a single RequestReadDirAsync call. This allows the caller to abort enumeration early if they satisfied their goal before reaching the end of enumeration.

Assuming the consumer is currently targeting net472, they would only have access to ListDirectoryAsync. If the consumer later changes its target to netstandard2.1 (or later, i.e. net5.0 or net6.0), they would still be able to call ListDirectoryAsync, but it will light up as obsolete. In addition, they would now be able to call EnumerateDirectoryAsync. This allows for gradual migration and does not present a breaking change for the caller when raising their target.

Any consumer of EnumerateDirectoryAsync would need to have access to async foreach, which is a C# 8.0 feature, meaning the consumer can target netstandard2.1 or later. So I don't see any realistic scenario for using IAsyncEnumerable in pre-netstandard2.1 world.

@IgorMilavec IgorMilavec marked this pull request as draft December 19, 2021 23:24
@IgorMilavec
Copy link
Collaborator Author

I did not change ISftpClient yet, so this PR is only a draft at this time...

@Kim-SSi
Copy link

Kim-SSi commented Dec 20, 2021

Depending on what targets you want to support.
For .NET >=4.6.1 & .NETStandard2.0 you could use Microsoft.Bcl.AsyncInterfaces
And for now maybe:
For .NET >=4.5 & .NETStandard2.0 you could use AsyncEnumerator

@IgorMilavec
Copy link
Collaborator Author

I think there is no point in introducing new features into SSH.NET that target non-supported runtimes. This basically limits our discussion to net472. Also, I think this should not be a discussion of whether we CAN support IAsyncEnumerable on net472, but rather a discussion of whether we SHOULD support it.

The only real life difference I see between Task<IEnumerable<SftpFIle>> and IAsyncENumerable<SftpFile> is in a scenario where you have a remote folder with a ton of files. I have a relatively large net472 solution in production where I use SSH.NET. I was able to negotiate the ability to (re)move remote files after the transfer in all but one case and even there the remote party will remove files after one week. This keeps the number of files small and under control. I really do not see a realistic real world scenario where not having IAsyncEnumerable on net472 would be a deal breaker for using SSH.NET.

Currently SSH.NET has no external dependencies (SshNet.Security.Cryptography is a sibling and under the same governance) when targeting net472 or netstandard2.0/2.1. I think we should keep it this way, at least it terms of the default package. If someone wants to do a private build, this is of course entirely up to him/her. We can even introduce a feature flag if it helps someone, but I really really would hope to stay dependency free if possible.

@drieseng
Copy link
Member

I also prefer to also keep the number of dependencies to an absolute minimum.
That said, I'd still like us to use IAsyncEnumerable.
Personally, I'm ok with only supporting this method on .NET Standard 2.1+.

@IgorMilavec IgorMilavec marked this pull request as ready for review December 22, 2021 18:28
@IgorMilavec
Copy link
Collaborator Author

Should we add IAsyncDisposable implementation while we're at it?

Copy link
Member

@drieseng drieseng left a comment

Choose a reason for hiding this comment

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

Let's "await" the result of the TFM discussion before we move ahead with this one.

#if NETSTANDARD2_1_OR_GREATER
[Obsolete("Use EnumerateDirectoryAsync()")]
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
#endif
Task<IEnumerable<SftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer to only expose the IAsyncEnumerable version (with ListDirectoryAsync as name), even if that means it's .NET Standard 2.1 (or higher) only.
Is the NETSTANDARD2_1_OR_GREATER symbol also defined for .NET 5.0 or 6.0?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If we reuse the same name with a different signature, this means a breaking change when the caller changes its target framework. If we do a "soft" transition with Obsoletes and a new name then we give callers time to transition to the new method at their own pace. But we do end up with a new name...
The symbol NETSTANDARD* is unfortunately not defined when targeting net5.0 or net6.0, so this check would need to be changed to #if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER.

<SignAssembly>true</SignAssembly>
<TargetFrameworks>net35;net40;net472;netstandard1.3;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net35;net40;net472;netstandard1.3;netstandard2.1</TargetFrameworks>
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to keep these (and other) changes to have a .NET Standard 2.1 flavor in this PR, or separate them out?
This PR would of course rely on that one being merged first.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

IMO it would be more clean if we first remove old targets and move to .NET Standard 2.0. We should probably also add .NET Standard 2.1 target in that PR, even without using 2.1 features. Later we can implement new 2.1 features in separate PRs (like this one) one feature at a time.

/// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
IAsyncEnumerable<SftpFile> EnumerateDirectoryAsync(string path, CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

This should return ISftpFile instead of the concrete SftpFile.

throw new ArgumentNullException(nameof(path));
if (_sftpSession == null)
throw new SshConnectionException("Client not connected.");
cancellationToken.ThrowIfCancellationRequested();
Copy link
Member

Choose a reason for hiding this comment

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

I suppose we perform this check in all async methods that we invoke here. Do we want this check here as well?

@WojciechNagorski
Copy link
Collaborator

I'm closing this PR because I've already removed old .NET frameworks and IAsyncEnumerable has been moved to #1126

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.

None yet

4 participants