Skip to content

Commit 828a48a

Browse files
thomhurstclaude
andauthored
feat: Add async overloads for File and Folder operations (#1722)
* feat: Add async overloads for File and Folder operations Fixes #1562 Adds async versions of sync-only file system operations: File.cs: - CreateAsync() - async file creation with proper stream disposal - DeleteAsync(CancellationToken) - async file deletion - MoveToAsync(string/Folder, CancellationToken) - async file move - CopyToAsync(string/Folder, CancellationToken) - async stream-based copy Folder.cs: - CreateAsync() - async folder creation - DeleteAsync(CancellationToken) - async recursive folder deletion - MoveToAsync(string, CancellationToken) - async folder move - CopyToAsync(string, bool, CancellationToken) - async stream-based folder copy Benefits: - Prevents thread pool starvation in highly concurrent pipelines - Uses stream-based copying for truly async I/O - Supports cancellation tokens for long-running operations - Consistent API with existing async methods (Read/Write) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Simplify await using syntax and document Task.Run usage Address review feedback: - Simplify CopyToAsync to use 'await using var' declarations (cleaner syntax) - Add <remarks> documenting that Task.Run is used for operations without native async APIs (.NET has no async file delete/move/directory create) - This is a common pattern for thread pool offloading of blocking I/O * fix: Add CancellationToken to Folder.CreateAsync Add optional CancellationToken parameter to CreateAsync for API consistency with other async methods in the class. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b2562f9 commit 828a48a

2 files changed

Lines changed: 234 additions & 0 deletions

File tree

src/ModularPipelines/FileSystem/File.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,19 @@ public File Create()
163163
return this;
164164
}
165165

166+
/// <summary>
167+
/// Asynchronously creates a new file at the current path.
168+
/// </summary>
169+
/// <returns>This file instance for method chaining.</returns>
170+
public async Task<File> CreateAsync()
171+
{
172+
LogFileOperation("Creating File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
173+
174+
var fileStream = System.IO.File.Create(Path);
175+
await fileStream.DisposeAsync().ConfigureAwait(false);
176+
return this;
177+
}
178+
166179
/// <inheritdoc cref="FileSystemInfo.Attributes"/>>
167180
public FileAttributes Attributes
168181
{
@@ -192,6 +205,21 @@ public void Delete()
192205
FileInfo.Delete();
193206
}
194207

208+
/// <summary>
209+
/// Asynchronously deletes the file.
210+
/// </summary>
211+
/// <remarks>
212+
/// Uses thread pool offloading as no native async delete API exists in .NET.
213+
/// For true async I/O, consider using stream-based operations where available.
214+
/// </remarks>
215+
/// <param name="cancellationToken">Cancellation token.</param>
216+
public Task DeleteAsync(CancellationToken cancellationToken = default)
217+
{
218+
LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
219+
220+
return Task.Run(() => FileInfo.Delete(), cancellationToken);
221+
}
222+
195223
/// <inheritdoc cref="FileInfo.MoveTo(string)"/>>
196224
public File MoveTo(string path)
197225
{
@@ -210,6 +238,43 @@ public File MoveTo(Folder folder)
210238
return MoveTo(System.IO.Path.Combine(folder.Path, Name));
211239
}
212240

241+
/// <summary>
242+
/// Asynchronously moves the file to a new path.
243+
/// </summary>
244+
/// <remarks>
245+
/// Uses thread pool offloading as no native async move API exists in .NET.
246+
/// </remarks>
247+
/// <param name="path">The destination path.</param>
248+
/// <param name="cancellationToken">Cancellation token.</param>
249+
/// <returns>This file instance for method chaining.</returns>
250+
public Task<File> MoveToAsync(string path, CancellationToken cancellationToken = default)
251+
{
252+
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
253+
254+
return Task.Run(() =>
255+
{
256+
FileInfo.MoveTo(path);
257+
return this;
258+
}, cancellationToken);
259+
}
260+
261+
/// <summary>
262+
/// Asynchronously moves the file to a folder.
263+
/// </summary>
264+
/// <remarks>
265+
/// Uses thread pool offloading as no native async move API exists in .NET.
266+
/// </remarks>
267+
/// <param name="folder">The destination folder.</param>
268+
/// <param name="cancellationToken">Cancellation token.</param>
269+
/// <returns>This file instance for method chaining.</returns>
270+
public async Task<File> MoveToAsync(Folder folder, CancellationToken cancellationToken = default)
271+
{
272+
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder);
273+
274+
await folder.CreateAsync().ConfigureAwait(false);
275+
return await MoveToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false);
276+
}
277+
213278
/// <inheritdoc cref="FileInfo.CopyTo(string)"/>>
214279
public File CopyTo(string path)
215280
{
@@ -226,6 +291,37 @@ public File CopyTo(Folder folder)
226291
return CopyTo(System.IO.Path.Combine(folder.Path, Name));
227292
}
228293

294+
/// <summary>
295+
/// Asynchronously copies the file to a new path using stream-based copying.
296+
/// </summary>
297+
/// <param name="path">The destination path.</param>
298+
/// <param name="cancellationToken">Cancellation token.</param>
299+
/// <returns>A new File instance representing the copied file.</returns>
300+
public async Task<File> CopyToAsync(string path, CancellationToken cancellationToken = default)
301+
{
302+
LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
303+
304+
await using var sourceStream = System.IO.File.OpenRead(Path);
305+
await using var destStream = System.IO.File.Create(path);
306+
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);
307+
308+
return new File(path);
309+
}
310+
311+
/// <summary>
312+
/// Asynchronously copies the file to a folder using stream-based copying.
313+
/// </summary>
314+
/// <param name="folder">The destination folder.</param>
315+
/// <param name="cancellationToken">Cancellation token.</param>
316+
/// <returns>A new File instance representing the copied file.</returns>
317+
public async Task<File> CopyToAsync(Folder folder, CancellationToken cancellationToken = default)
318+
{
319+
LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder);
320+
321+
await folder.CreateAsync().ConfigureAwait(false);
322+
return await CopyToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false);
323+
}
324+
229325
public static File GetNewTemporaryFilePath()
230326
{
231327
var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());

src/ModularPipelines/FileSystem/Folder.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,46 @@ public Folder Create()
9696
return this;
9797
}
9898

99+
/// <summary>
100+
/// Asynchronously creates the folder if it does not exist.
101+
/// </summary>
102+
/// <remarks>
103+
/// Uses thread pool offloading as no native async directory creation API exists in .NET.
104+
/// </remarks>
105+
/// <param name="cancellationToken">Optional cancellation token.</param>
106+
/// <returns>This folder instance for method chaining.</returns>
107+
public Task<Folder> CreateAsync(CancellationToken cancellationToken = default)
108+
{
109+
LogFolderOperation("Creating Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
110+
111+
return Task.Run(() =>
112+
{
113+
Directory.CreateDirectory(Path);
114+
return this;
115+
}, cancellationToken);
116+
}
117+
99118
public void Delete()
100119
{
101120
LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
102121

103122
DirectoryInfo.Delete(true);
104123
}
105124

125+
/// <summary>
126+
/// Asynchronously deletes the folder and all its contents.
127+
/// </summary>
128+
/// <remarks>
129+
/// Uses thread pool offloading as no native async delete API exists in .NET.
130+
/// </remarks>
131+
/// <param name="cancellationToken">Cancellation token.</param>
132+
public Task DeleteAsync(CancellationToken cancellationToken = default)
133+
{
134+
LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
135+
136+
return Task.Run(() => DirectoryInfo.Delete(true), cancellationToken);
137+
}
138+
106139
/// <summary>
107140
/// Removes all files and subdirectories within the folder.
108141
/// </summary>
@@ -270,6 +303,91 @@ public Folder CopyTo(string targetPath, bool preserveTimestamps)
270303
return new Folder(targetPath);
271304
}
272305

306+
/// <summary>
307+
/// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying.
308+
/// </summary>
309+
/// <param name="targetPath">The destination path for the copied folder.</param>
310+
/// <param name="cancellationToken">Cancellation token.</param>
311+
/// <returns>A new <see cref="Folder"/> instance representing the copied folder.</returns>
312+
public Task<Folder> CopyToAsync(string targetPath, CancellationToken cancellationToken = default)
313+
{
314+
return CopyToAsync(targetPath, preserveTimestamps: false, cancellationToken);
315+
}
316+
317+
/// <summary>
318+
/// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying.
319+
/// </summary>
320+
/// <param name="targetPath">The destination path for the copied folder.</param>
321+
/// <param name="preserveTimestamps">
322+
/// When true, preserves CreationTimeUtc, LastWriteTimeUtc, and LastAccessTimeUtc
323+
/// for all files and directories.
324+
/// </param>
325+
/// <param name="cancellationToken">Cancellation token.</param>
326+
/// <returns>A new <see cref="Folder"/> instance representing the copied folder.</returns>
327+
public async Task<Folder> CopyToAsync(string targetPath, bool preserveTimestamps, CancellationToken cancellationToken = default)
328+
{
329+
LogFolderOperationWithDestination("Copying Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, targetPath);
330+
331+
Directory.CreateDirectory(targetPath);
332+
333+
// Copy all subdirectories first
334+
foreach (var dirPath in Directory.EnumerateDirectories(this, "*", SearchOption.AllDirectories))
335+
{
336+
cancellationToken.ThrowIfCancellationRequested();
337+
338+
var sourceDir = new DirectoryInfo(dirPath);
339+
var relativePath = System.IO.Path.GetRelativePath(this, dirPath);
340+
var newPath = System.IO.Path.Combine(targetPath, relativePath);
341+
var targetDir = Directory.CreateDirectory(newPath);
342+
343+
targetDir.Attributes = sourceDir.Attributes;
344+
345+
if (preserveTimestamps)
346+
{
347+
targetDir.CreationTimeUtc = sourceDir.CreationTimeUtc;
348+
targetDir.LastWriteTimeUtc = sourceDir.LastWriteTimeUtc;
349+
targetDir.LastAccessTimeUtc = sourceDir.LastAccessTimeUtc;
350+
}
351+
}
352+
353+
// Copy all files using async stream copying
354+
foreach (var filePath in Directory.EnumerateFiles(this, "*", SearchOption.AllDirectories))
355+
{
356+
cancellationToken.ThrowIfCancellationRequested();
357+
358+
var sourceFile = new FileInfo(filePath);
359+
var relativePath = System.IO.Path.GetRelativePath(this, filePath);
360+
var newPath = System.IO.Path.Combine(targetPath, relativePath);
361+
362+
await using var sourceStream = System.IO.File.OpenRead(filePath);
363+
await using var destStream = System.IO.File.Create(newPath);
364+
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);
365+
366+
var targetFile = new FileInfo(newPath);
367+
targetFile.Attributes = sourceFile.Attributes;
368+
369+
if (preserveTimestamps)
370+
{
371+
targetFile.CreationTimeUtc = sourceFile.CreationTimeUtc;
372+
targetFile.LastWriteTimeUtc = sourceFile.LastWriteTimeUtc;
373+
targetFile.LastAccessTimeUtc = sourceFile.LastAccessTimeUtc;
374+
}
375+
}
376+
377+
// Preserve root directory attributes and timestamps
378+
var targetRootDir = new DirectoryInfo(targetPath);
379+
targetRootDir.Attributes = DirectoryInfo.Attributes;
380+
381+
if (preserveTimestamps)
382+
{
383+
targetRootDir.CreationTimeUtc = DirectoryInfo.CreationTimeUtc;
384+
targetRootDir.LastWriteTimeUtc = DirectoryInfo.LastWriteTimeUtc;
385+
targetRootDir.LastAccessTimeUtc = DirectoryInfo.LastAccessTimeUtc;
386+
}
387+
388+
return new Folder(targetPath);
389+
}
390+
273391
public Folder MoveTo(string path)
274392
{
275393
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
@@ -278,6 +396,26 @@ public Folder MoveTo(string path)
278396
return this;
279397
}
280398

399+
/// <summary>
400+
/// Asynchronously moves the folder to a new path.
401+
/// </summary>
402+
/// <remarks>
403+
/// Uses thread pool offloading as no native async move API exists in .NET.
404+
/// </remarks>
405+
/// <param name="path">The destination path.</param>
406+
/// <param name="cancellationToken">Cancellation token.</param>
407+
/// <returns>This folder instance for method chaining.</returns>
408+
public Task<Folder> MoveToAsync(string path, CancellationToken cancellationToken = default)
409+
{
410+
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
411+
412+
return Task.Run(() =>
413+
{
414+
DirectoryInfo.MoveTo(path);
415+
return this;
416+
}, cancellationToken);
417+
}
418+
281419
public Folder GetFolder(string name)
282420
{
283421
var directoryInfo = new DirectoryInfo(System.IO.Path.Combine(Path, name));

0 commit comments

Comments
 (0)