Skip to content

Generate ten synchronous SftpClient methods from their async versions - #1812

Open
virzak wants to merge 1 commit into
sshnet:developfrom
virzak:feature/generate-sync-sftp-methods
Open

Generate ten synchronous SftpClient methods from their async versions#1812
virzak wants to merge 1 commit into
sshnet:developfrom
virzak:feature/generate-sync-sftp-methods

Conversation

@virzak

@virzak virzak commented Jul 28, 2026

Copy link
Copy Markdown

What this is

Ten of the synchronous methods on SftpClient are mechanical translations of the asynchronous ones beside them - the same argument checks, the same session calls with Async dropped, the same order. Each is maintained by hand next to its twin, so every fix has to be made twice.

This generates the synchronous version from the asynchronous source at compile time, into the same partial class. The public API is unchanged.

DeleteFile is representative:

public async Task DeleteFileAsync(string path, CancellationToken cancellationToken)
{
    CheckDisposed();
    ArgumentException.ThrowIfNullOrWhiteSpace(path);

    if (_sftpSession is null)
    {
        throw new SshConnectionException("Client not connected.");
    }

    cancellationToken.ThrowIfCancellationRequested();

    var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
    await _sftpSession.RequestRemoveAsync(fullPath, cancellationToken).ConfigureAwait(false);
}

await is unwrapped, Task becomes void, the token parameter and the statements which only exist to observe it are dropped, GetCanonicalPathAsync resolves to GetCanonicalPath, and what comes out is exactly the DeleteFile which is in the file today.

Verification

I compared each generated method against the hand-written one it replaces, reducing the fully qualified names the generator emits. All ten are identical - not equivalent, identical. The methods are ChangeDirectory, CreateDirectory, DeleteDirectory, DeleteFile, Get, Exists, GetStatus, Delete, Open(string, FileMode, FileAccess) and GetAttributes.

  • dotnet build Renci.SshNet.slnx -c Release - clean, no new warnings, across all five target frameworks
  • Renci.SshNet.Tests on net10.0 - 2312 passed, 0 failed, 12 skipped
  • Integration tests not run here; they need Docker, which I do not have set up for this repository

To read what is generated:

dotnet build src/Renci.SshNet/Renci.SshNet.csproj -p:EmitCompilerGeneratedFiles=true

Documentation

The converted async methods now carry <inheritdoc /> where they previously repeated what ISftpClient already declares. That is the pattern DeleteFileAsync and DeleteDirectoryAsync already use, and the generated synchronous methods inherit the same documentation from the interface. Published documentation is unchanged - worth stating plainly, since the docs site is built from these.

Deliberately left alone

  • RenameFile - the synchronous version routes through an isPosix overload which the async version has no equivalent of, so the two are not twins.
  • DownloadFile, UploadFile, ListDirectory - these genuinely differ between the two versions rather than being translations.

SftpSession has another 16 pairs in the same shape. I have not touched them; if this lands well, that is the obvious follow-up.

About the dependency

Zomp.SyncMethodGenerator is a Roslyn source generator, referenced with PrivateAssets="all", so it adds nothing to the shipped package or the runtime graph. SharpCompress adopted it earlier this week for the same reason.

Disclosure: I maintain the generator. Two fixes went into it while preparing this PR - crefs in generated documentation not resolving, which would otherwise have been a build failure here, and argument list spacing. I am proposing this because the duplication in this file is exactly the case the tool handles, but if you would rather not take a build-time dependency, no hard feelings and I will close it.

🤖 Generated with Claude Code

Ten of the synchronous methods on SftpClient are mechanical translations of
the asynchronous ones beside them: the same argument checks, the same session
calls with Async dropped, the same order. Each has been maintained by hand
alongside its twin, and every fix to one has to be made to the other.

Zomp.SyncMethodGenerator produces the synchronous version from the async
source at compile time, into the same partial class, so the public API is
unchanged. Verified method by method: each generated body is identical to the
hand-written one it replaces, once the fully qualified names are reduced.

The async methods now carry <inheritdoc /> where they previously repeated the
interface documentation, which is what the twins already converted use, and
the generated methods inherit the same documentation ISftpClient declares.
Published documentation is unchanged.

Left alone: RenameFile, whose async version does not have the isPosix overload
the synchronous one routes through; and DownloadFile, UploadFile and
ListDirectory, whose two versions genuinely differ.

The generator is a build-time only dependency, so nothing is added to the
package or to the runtime graph.
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.

1 participant