using System.Runtime.CompilerServices; using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Core.Helpers; public static class SharePointPaginationHelper { // Max page size SharePoint honors with Paged='TRUE' (threshold bypass). private const int DefaultRowLimit = 5000; /// /// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold. /// Uses CamlQuery with Paged='TRUE' RowLimit and ListItemCollectionPosition for pagination. /// Never call ExecuteQuery directly on a list — always use this helper. /// public static async IAsyncEnumerable GetAllItemsAsync( ClientContext ctx, List list, CamlQuery? baseQuery = null, [EnumeratorCancellation] CancellationToken ct = default) { var query = baseQuery ?? CamlQuery.CreateAllItemsQuery(); query.ViewXml = BuildPagedViewXml(query.ViewXml, DefaultRowLimit); 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); } /// /// Enumerates items within a specific folder (direct children by default, or /// recursive when is true). Uses paginated CAML /// with no WHERE clause so it works on libraries above the 5,000-item threshold. /// Callers filter by FSObjType client-side via the returned ListItem fields. /// public static async IAsyncEnumerable GetItemsInFolderAsync( ClientContext ctx, List list, string folderServerRelativeUrl, bool recursive, string[]? viewFields = null, [EnumeratorCancellation] CancellationToken ct = default) { var fields = viewFields ?? new[] { "FSObjType", "FileRef", "FileLeafRef", "FileDirRef", "File_x0020_Size" }; var viewFieldsXml = string.Join(string.Empty, fields.Select(f => $"")); var scope = recursive ? " Scope='RecursiveAll'" : string.Empty; var viewXml = $"" + "" + $"{viewFieldsXml}" + $"{DefaultRowLimit}" + ""; var query = new CamlQuery { ViewXml = viewXml, FolderServerRelativeUrl = folderServerRelativeUrl, 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); } internal static string BuildPagedViewXml(string? existingXml, int rowLimit) { if (string.IsNullOrWhiteSpace(existingXml)) return $"{rowLimit}"; // Replace any existing n with paged form. if (System.Text.RegularExpressions.Regex.IsMatch( existingXml, @"]*>\d+", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) { return System.Text.RegularExpressions.Regex.Replace( existingXml, @"]*>\d+", $"{rowLimit}", System.Text.RegularExpressions.RegexOptions.IgnoreCase); } return existingXml.Replace("", $"{rowLimit}", StringComparison.OrdinalIgnoreCase); } }