f4cc81bb71
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager) - Add InputDialog, Spinner common view - Add DuplicatesCsvExportService - Refresh views, dialogs, and view models across tabs - Update localization strings (en/fr) - Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
2.9 KiB
C#
80 lines
2.9 KiB
C#
using SharepointToolbox.Core.Models;
|
|
|
|
namespace SharepointToolbox.Services;
|
|
|
|
public static class BulkOperationRunner
|
|
{
|
|
/// <summary>
|
|
/// 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 <paramref name="maxConcurrency"/> > 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).
|
|
/// </summary>
|
|
public static async Task<BulkOperationSummary<TItem>> RunAsync<TItem>(
|
|
IReadOnlyList<TItem> items,
|
|
Func<TItem, int, CancellationToken, Task> processItem,
|
|
IProgress<OperationProgress> progress,
|
|
CancellationToken ct,
|
|
int maxConcurrency = 1)
|
|
{
|
|
if (items.Count == 0)
|
|
{
|
|
progress.Report(new OperationProgress(0, 0, "Nothing to do."));
|
|
return new BulkOperationSummary<TItem>(Array.Empty<BulkItemResult<TItem>>());
|
|
}
|
|
|
|
progress.Report(new OperationProgress(0, items.Count, $"Processing 1/{items.Count}..."));
|
|
|
|
var results = new BulkItemResult<TItem>[items.Count];
|
|
int completed = 0;
|
|
|
|
async Task RunOne(int i, CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
await processItem(items[i], i, token);
|
|
results[i] = BulkItemResult<TItem>.Success(items[i]);
|
|
}
|
|
catch (OperationCanceledException) { throw; }
|
|
catch (Exception ex)
|
|
{
|
|
results[i] = BulkItemResult<TItem>.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<TItem>(results);
|
|
}
|
|
}
|