using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public static class BulkOperationRunner { /// /// Runs a bulk operation with continue-on-error semantics, per-item result tracking, /// and cancellation support. OperationCanceledException propagates immediately. /// /// Progress is reported AFTER each item completes (success or failure), so the bar /// reflects actual work done rather than work queued. A final "Complete" report /// guarantees 100% when the total was determinate. /// /// Set > 1 to run items in parallel. Callers must /// ensure processItem is safe to invoke concurrently (e.g. each invocation uses its /// own CSOM ClientContext — a shared CSOM context is NOT thread-safe). /// public static async Task> RunAsync( IReadOnlyList items, Func processItem, IProgress progress, CancellationToken ct, int maxConcurrency = 1) { if (items.Count == 0) { progress.Report(new OperationProgress(0, 0, "Nothing to do.")); return new BulkOperationSummary(Array.Empty>()); } progress.Report(new OperationProgress(0, items.Count, $"Processing 1/{items.Count}...")); var results = new BulkItemResult[items.Count]; int completed = 0; async Task RunOne(int i, CancellationToken token) { try { await processItem(items[i], i, token); results[i] = BulkItemResult.Success(items[i]); } catch (OperationCanceledException) { throw; } catch (Exception ex) { results[i] = BulkItemResult.Failed(items[i], ex.Message); } finally { int done = Interlocked.Increment(ref completed); progress.Report(new OperationProgress(done, items.Count, $"Processed {done}/{items.Count}")); } } if (maxConcurrency <= 1) { for (int i = 0; i < items.Count; i++) { ct.ThrowIfCancellationRequested(); await RunOne(i, ct); } } else { var options = new ParallelOptions { MaxDegreeOfParallelism = maxConcurrency, CancellationToken = ct }; await Parallel.ForEachAsync(Enumerable.Range(0, items.Count), options, async (i, token) => await RunOne(i, token)); } progress.Report(new OperationProgress(items.Count, items.Count, "Complete.")); return new BulkOperationSummary(results); } }