- 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
57 lines
2.1 KiB
C#
57 lines
2.1 KiB
C#
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);
|
|
}
|
|
}
|