Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/ModularPipelines/FileSystem/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ public File Create()
return this;
}

/// <summary>
/// Asynchronously creates a new file at the current path.
/// </summary>
/// <returns>This file instance for method chaining.</returns>
public async Task<File> CreateAsync()
{
LogFileOperation("Creating File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);

var fileStream = System.IO.File.Create(Path);
await fileStream.DisposeAsync().ConfigureAwait(false);
return this;
}
Comment on lines +170 to +177
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new CreateAsync method lacks test coverage. Since the synchronous Create method has comprehensive test coverage in FileTests.cs, the async version should have corresponding tests to verify it works correctly and handles edge cases like creating files in non-existent directories.

Copilot uses AI. Check for mistakes.

/// <inheritdoc cref="FileSystemInfo.Attributes"/>>
public FileAttributes Attributes
{
Expand Down Expand Up @@ -192,6 +205,21 @@ public void Delete()
FileInfo.Delete();
}

/// <summary>
/// Asynchronously deletes the file.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async delete API exists in .NET.
/// For true async I/O, consider using stream-based operations where available.
/// </remarks>
/// <param name="cancellationToken">Cancellation token.</param>
public Task DeleteAsync(CancellationToken cancellationToken = default)
{
Comment on lines +212 to +217
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new DeleteAsync method lacks test coverage. Since the synchronous Delete method has comprehensive test coverage in FileTests.cs, the async version should have corresponding tests to verify it works correctly and properly handles cancellation tokens.

Copilot uses AI. Check for mistakes.
LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);

return Task.Run(() => FileInfo.Delete(), cancellationToken);
Comment on lines +212 to +220
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This implementation wraps a synchronous operation (FileInfo.Delete) in Task.Run, which contradicts the PR's goal of preventing thread pool starvation. File deletion is typically a fast metadata operation that doesn't benefit from being wrapped in Task.Run, and doing so actually increases thread pool pressure.

Suggested change
/// Uses thread pool offloading as no native async delete API exists in .NET.
/// For true async I/O, consider using stream-based operations where available.
/// </remarks>
/// <param name="cancellationToken">Cancellation token.</param>
public Task DeleteAsync(CancellationToken cancellationToken = default)
{
LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
return Task.Run(() => FileInfo.Delete(), cancellationToken);
/// No native async delete API exists in .NET; this method performs a synchronous
/// delete and returns a completed task. For true async I/O, consider using
/// stream-based operations where available.
/// </remarks>
/// <param name="cancellationToken">Cancellation token.</param>
public Task DeleteAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
LogFileOperation("Deleting File: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);
FileInfo.Delete();
return Task.CompletedTask;

Copilot uses AI. Check for mistakes.
}

/// <inheritdoc cref="FileInfo.MoveTo(string)"/>>
public File MoveTo(string path)
{
Expand All @@ -210,6 +238,43 @@ public File MoveTo(Folder folder)
return MoveTo(System.IO.Path.Combine(folder.Path, Name));
}

/// <summary>
/// Asynchronously moves the file to a new path.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async move API exists in .NET.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This file instance for method chaining.</returns>
public Task<File> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
Comment on lines +243 to +252
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new MoveToAsync method lacks test coverage. Since the synchronous MoveTo method has comprehensive test coverage in FileTests.cs, the async version should have corresponding tests to verify it works correctly, properly handles cancellation tokens, and updates the file path as expected.

Copilot uses AI. Check for mistakes.

return Task.Run(() =>
{
FileInfo.MoveTo(path);
return this;
}, cancellationToken);
Comment on lines +245 to +258
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This implementation wraps a synchronous operation (FileInfo.MoveTo) in Task.Run, which contradicts the PR's goal of preventing thread pool starvation. File move operations are typically fast metadata operations that don't benefit from being wrapped in Task.Run, and doing so actually increases thread pool pressure.

Suggested change
/// Uses thread pool offloading as no native async move API exists in .NET.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This file instance for method chaining.</returns>
public Task<File> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
return Task.Run(() =>
{
FileInfo.MoveTo(path);
return this;
}, cancellationToken);
/// Executes the move synchronously as file moves are typically fast metadata operations.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This file instance for method chaining.</returns>
public Task<File> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
FileInfo.MoveTo(path);
return Task.FromResult(this);

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// Asynchronously moves the file to a folder.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async move API exists in .NET.
/// </remarks>
Comment on lines +260 to +266
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new MoveToAsync(Folder) overload lacks test coverage. Since the synchronous MoveTo(Folder) method has test coverage in FileTests.cs, the async version should have corresponding tests to verify it correctly creates the destination folder and moves the file.

Copilot uses AI. Check for mistakes.
/// <param name="folder">The destination folder.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This file instance for method chaining.</returns>
public async Task<File> MoveToAsync(Folder folder, CancellationToken cancellationToken = default)
{
LogFileOperationWithDestination("Moving File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder);

await folder.CreateAsync().ConfigureAwait(false);
return await MoveToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc cref="FileInfo.CopyTo(string)"/>>
public File CopyTo(string path)
{
Expand All @@ -226,6 +291,37 @@ public File CopyTo(Folder folder)
return CopyTo(System.IO.Path.Combine(folder.Path, Name));
}

/// <summary>
/// Asynchronously copies the file to a new path using stream-based copying.
/// </summary>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A new File instance representing the copied file.</returns>
public async Task<File> CopyToAsync(string path, CancellationToken cancellationToken = default)
{
LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
Comment on lines +294 to +302
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The file streams are not properly disposed if an exception occurs between opening the source stream and the await using block. Consider declaring the variable inside the await using statement or using a try-finally block to ensure proper disposal in all error cases.

Copilot uses AI. Check for mistakes.

await using var sourceStream = System.IO.File.OpenRead(Path);
await using var destStream = System.IO.File.Create(path);
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);

return new File(path);
}

/// <summary>
/// Asynchronously copies the file to a folder using stream-based copying.
/// </summary>
/// <param name="folder">The destination folder.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A new File instance representing the copied file.</returns>
public async Task<File> CopyToAsync(Folder folder, CancellationToken cancellationToken = default)
{
LogFileOperationWithDestination("Copying File: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, folder);
Comment on lines +313 to +319
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new CopyToAsync(Folder) overload lacks test coverage. Since the synchronous CopyTo(Folder) method has test coverage in FileTests.cs, the async version should have corresponding tests to verify it correctly creates the destination folder and copies the file with proper content preservation.

Copilot uses AI. Check for mistakes.

await folder.CreateAsync().ConfigureAwait(false);
return await CopyToAsync(System.IO.Path.Combine(folder.Path, Name), cancellationToken).ConfigureAwait(false);
}

public static File GetNewTemporaryFilePath()
{
var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
Expand Down
138 changes: 138 additions & 0 deletions src/ModularPipelines/FileSystem/Folder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,46 @@
return this;
}

/// <summary>
/// Asynchronously creates the folder if it does not exist.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async directory creation API exists in .NET.
/// </remarks>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>This folder instance for method chaining.</returns>
public Task<Folder> CreateAsync(CancellationToken cancellationToken = default)
{
LogFolderOperation("Creating Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);

return Task.Run(() =>
{
Directory.CreateDirectory(Path);
return this;
}, cancellationToken);
}

public void Delete()
{
LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);

DirectoryInfo.Delete(true);
}

/// <summary>
/// Asynchronously deletes the folder and all its contents.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async delete API exists in .NET.
/// </remarks>
/// <param name="cancellationToken">Cancellation token.</param>
Comment on lines +126 to +131
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This implementation wraps a synchronous operation (DirectoryInfo.Delete) in Task.Run, which contradicts the PR's goal of preventing thread pool starvation. Wrapping synchronous operations in Task.Run actually increases thread pool usage rather than reducing it. The PR description claims stream-based async operations, but this is just offloading sync work to the thread pool.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +131
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new DeleteAsync method lacks test coverage. Since folder operations have comprehensive test coverage in FolderTests.cs, the async version should have corresponding tests to verify it works correctly, properly handles cancellation tokens, and deletes all contents recursively.

Copilot uses AI. Check for mistakes.
public Task DeleteAsync(CancellationToken cancellationToken = default)
{
LogFolderOperation("Deleting Folder: {Path} [Module: {ModuleName}, Activity: {ActivityId}]", this);

return Task.Run(() => DirectoryInfo.Delete(true), cancellationToken);
}

/// <summary>
/// Removes all files and subdirectories within the folder.
/// </summary>
Expand Down Expand Up @@ -217,10 +250,10 @@
Directory.CreateDirectory(targetPath);

// Copy all subdirectories first
foreach (var dirPath in Directory.EnumerateDirectories(this, "*", SearchOption.AllDirectories))

Check warning on line 253 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)'.

Check warning on line 253 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)'.
{
var sourceDir = new DirectoryInfo(dirPath);
var relativePath = System.IO.Path.GetRelativePath(this, dirPath);

Check warning on line 256 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.

Check warning on line 256 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.
var newPath = System.IO.Path.Combine(targetPath, relativePath);
var targetDir = Directory.CreateDirectory(newPath);

Expand All @@ -236,10 +269,10 @@
}

// Copy all files
foreach (var filePath in Directory.EnumerateFiles(this, "*", SearchOption.AllDirectories))

Check warning on line 272 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateFiles(string path, string searchPattern, SearchOption searchOption)'.

Check warning on line 272 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateFiles(string path, string searchPattern, SearchOption searchOption)'.
{
var sourceFile = new FileInfo(filePath);
var relativePath = System.IO.Path.GetRelativePath(this, filePath);

Check warning on line 275 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.

Check warning on line 275 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.
var newPath = System.IO.Path.Combine(targetPath, relativePath);
System.IO.File.Copy(filePath, newPath, true);

Expand Down Expand Up @@ -270,6 +303,91 @@
return new Folder(targetPath);
}

/// <summary>
/// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying.
/// </summary>
/// <param name="targetPath">The destination path for the copied folder.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A new <see cref="Folder"/> instance representing the copied folder.</returns>
public Task<Folder> CopyToAsync(string targetPath, CancellationToken cancellationToken = default)
{
return CopyToAsync(targetPath, preserveTimestamps: false, cancellationToken);
}

/// <summary>
/// Asynchronously copies the folder and its contents to the specified target path using stream-based file copying.
/// </summary>
/// <param name="targetPath">The destination path for the copied folder.</param>
/// <param name="preserveTimestamps">
/// When true, preserves CreationTimeUtc, LastWriteTimeUtc, and LastAccessTimeUtc
/// for all files and directories.
/// </param>
/// <param name="cancellationToken">Cancellation token.</param>
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This synchronous Directory.CreateDirectory call should be wrapped in Task.Run or use an async alternative to be consistent with the async nature of this method. This blocks the calling thread unnecessarily.

Copilot uses AI. Check for mistakes.
/// <returns>A new <see cref="Folder"/> instance representing the copied folder.</returns>
public async Task<Folder> CopyToAsync(string targetPath, bool preserveTimestamps, CancellationToken cancellationToken = default)
{
LogFolderOperationWithDestination("Copying Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, targetPath);

Directory.CreateDirectory(targetPath);

// Copy all subdirectories first
foreach (var dirPath in Directory.EnumerateDirectories(this, "*", SearchOption.AllDirectories))

Check warning on line 334 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)'.

Check warning on line 334 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'path' in 'IEnumerable<string> Directory.EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)'.
{
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This synchronous Directory.CreateDirectory call should be wrapped in Task.Run or use an async alternative to be consistent with the async nature of this method. This blocks the calling thread unnecessarily.

Copilot uses AI. Check for mistakes.
cancellationToken.ThrowIfCancellationRequested();

var sourceDir = new DirectoryInfo(dirPath);
var relativePath = System.IO.Path.GetRelativePath(this, dirPath);

Check warning on line 339 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (ubuntu-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.

Check warning on line 339 in src/ModularPipelines/FileSystem/Folder.cs

View workflow job for this annotation

GitHub Actions / pipeline (macos-latest)

Possible null reference argument for parameter 'relativeTo' in 'string Path.GetRelativePath(string relativeTo, string path)'.
var newPath = System.IO.Path.Combine(targetPath, relativePath);
var targetDir = Directory.CreateDirectory(newPath);

targetDir.Attributes = sourceDir.Attributes;

if (preserveTimestamps)
Comment on lines +328 to +345
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This synchronous enumeration with Directory.EnumerateDirectories blocks the calling thread. Consider using an approach that processes directories asynchronously, or at minimum, document that this portion is synchronous despite being in an async method.

Copilot uses AI. Check for mistakes.
{
targetDir.CreationTimeUtc = sourceDir.CreationTimeUtc;
targetDir.LastWriteTimeUtc = sourceDir.LastWriteTimeUtc;
targetDir.LastAccessTimeUtc = sourceDir.LastAccessTimeUtc;
}
}

// Copy all files using async stream copying
foreach (var filePath in Directory.EnumerateFiles(this, "*", SearchOption.AllDirectories))
{
cancellationToken.ThrowIfCancellationRequested();

var sourceFile = new FileInfo(filePath);
var relativePath = System.IO.Path.GetRelativePath(this, filePath);
var newPath = System.IO.Path.Combine(targetPath, relativePath);

await using var sourceStream = System.IO.File.OpenRead(filePath);
await using var destStream = System.IO.File.Create(newPath);
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);
Comment on lines +356 to +364
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The file streams are not properly disposed if an exception occurs between opening the source stream and the await using block. Consider declaring the variable inside the await using statement or using a try-finally block to ensure proper disposal in all error cases.

Copilot uses AI. Check for mistakes.

var targetFile = new FileInfo(newPath);
targetFile.Attributes = sourceFile.Attributes;

if (preserveTimestamps)
{
targetFile.CreationTimeUtc = sourceFile.CreationTimeUtc;
targetFile.LastWriteTimeUtc = sourceFile.LastWriteTimeUtc;
targetFile.LastAccessTimeUtc = sourceFile.LastAccessTimeUtc;
}
}
Comment on lines +348 to +375
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This synchronous enumeration with Directory.EnumerateFiles blocks the calling thread. Consider using an approach that processes files asynchronously, or at minimum, document that the enumeration portion is synchronous despite being in an async method.

Copilot uses AI. Check for mistakes.

// Preserve root directory attributes and timestamps
var targetRootDir = new DirectoryInfo(targetPath);
targetRootDir.Attributes = DirectoryInfo.Attributes;

if (preserveTimestamps)
{
targetRootDir.CreationTimeUtc = DirectoryInfo.CreationTimeUtc;
targetRootDir.LastWriteTimeUtc = DirectoryInfo.LastWriteTimeUtc;
targetRootDir.LastAccessTimeUtc = DirectoryInfo.LastAccessTimeUtc;
}

return new Folder(targetPath);
}
Comment on lines +306 to +389
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new CopyToAsync methods lack test coverage. Since folder operations have comprehensive test coverage in FolderTests.cs, the async versions should have corresponding tests to verify stream-based copying works correctly, handles cancellation tokens properly, preserves file content and timestamps when requested, and correctly copies nested directory structures.

Copilot uses AI. Check for mistakes.

public Folder MoveTo(string path)
{
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
Expand All @@ -278,6 +396,26 @@
return this;
}

/// <summary>
/// Asynchronously moves the folder to a new path.
/// </summary>
/// <remarks>
/// Uses thread pool offloading as no native async move API exists in .NET.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This folder instance for method chaining.</returns>
public Task<Folder> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);

return Task.Run(() =>
{
DirectoryInfo.MoveTo(path);
Comment on lines +405 to +414
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

The new MoveToAsync method lacks test coverage. Since folder operations have comprehensive test coverage in FolderTests.cs, the async version should have corresponding tests to verify it works correctly, properly handles cancellation tokens, and updates the folder path as expected.

Copilot uses AI. Check for mistakes.
return this;
}, cancellationToken);
Comment on lines +403 to +416
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

This implementation wraps a synchronous operation (DirectoryInfo.MoveTo) in Task.Run, which contradicts the PR's goal of preventing thread pool starvation. Directory move operations are typically fast metadata operations that don't benefit from being wrapped in Task.Run, and doing so actually increases thread pool pressure.

Suggested change
/// Uses thread pool offloading as no native async move API exists in .NET.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This folder instance for method chaining.</returns>
public Task<Folder> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
return Task.Run(() =>
{
DirectoryInfo.MoveTo(path);
return this;
}, cancellationToken);
/// Performs a synchronous move operation and returns a completed task for API consistency.
/// </remarks>
/// <param name="path">The destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>This folder instance for method chaining.</returns>
public Task<Folder> MoveToAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
LogFolderOperationWithDestination("Moving Folder: {Source} > {Destination} [Module: {ModuleName}, Activity: {ActivityId}]", this, path);
DirectoryInfo.MoveTo(path);
return Task.FromResult(this);

Copilot uses AI. Check for mistakes.
}

public Folder GetFolder(string name)
{
var directoryInfo = new DirectoryInfo(System.IO.Path.Combine(Path, name));
Expand Down
Loading