feat(01-02): add SharePointPaginationHelper, ExecuteQueryRetryHelper, LogPanelSink

- SharePointPaginationHelper: async iterator with ListItemCollectionPosition loop (bypasses 5k limit); RowLimit=2000; [EnumeratorCancellation] for correct WithCancellation support
- ExecuteQueryRetryHelper: exponential backoff on 429/503/throttle; surfaces retry events via IProgress<OperationProgress>; max 5 retries
- LogPanelSink: custom Serilog ILogEventSink writing color-coded entries to RichTextBox via Dispatcher.InvokeAsync for thread safety
This commit is contained in:
Dev
2026-04-02 12:06:39 +02:00
parent ddb216b1fb
commit c2978016b0
3 changed files with 150 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
using System.Runtime.CompilerServices;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Core.Helpers;
public static class SharePointPaginationHelper
{
/// <summary>
/// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold.
/// Uses CamlQuery with RowLimit=2000 and ListItemCollectionPosition for pagination.
/// Never call ExecuteQuery directly on a list — always use this helper.
/// </summary>
public static async IAsyncEnumerable<ListItem> GetAllItemsAsync(
ClientContext ctx,
List list,
CamlQuery? baseQuery = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var query = baseQuery ?? CamlQuery.CreateAllItemsQuery();
query.ViewXml = BuildPagedViewXml(query.ViewXml, rowLimit: 2000);
query.ListItemCollectionPosition = null;
do
{
ct.ThrowIfCancellationRequested();
var items = list.GetItems(query);
ctx.Load(items);
await ctx.ExecuteQueryAsync();
foreach (var item in items)
yield return item;
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
}
while (query.ListItemCollectionPosition != null);
}
private static string BuildPagedViewXml(string? existingXml, int rowLimit)
{
// Inject or replace RowLimit in existing CAML, or create minimal view
if (string.IsNullOrWhiteSpace(existingXml))
return $"<View><RowLimit>{rowLimit}</RowLimit></View>";
// Simple replacement approach — adequate for Phase 1
if (existingXml.Contains("<RowLimit>", StringComparison.OrdinalIgnoreCase))
{
return System.Text.RegularExpressions.Regex.Replace(
existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
$"<RowLimit>{rowLimit}</RowLimit>",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
}
return existingXml.Replace("</View>", $"<RowLimit>{rowLimit}</RowLimit></View>",
StringComparison.OrdinalIgnoreCase);
}
}