Linq to SharePoint. Создаем ContentIterator

В SharePoint 2010 для работы с большими списками есть класс Microsoft.Office.Server.Utilities.ContentIterator (Microsoft.Office.Server.dll), который позволяет итеративно обрабатывать элементы списка. Сегодня я покажу как можно реализовать аналогичный итератор при использовании Linq to SharePoint.

О самом ContentIterator'е можно прочитать у Алексея Садомова в посте Использование класса ContentIterator в разработке для Sharepoint.

Дроссельный контроль 2010

В SharePoint 2010 установлен лимит на выбор данных из списков/библиотек документов. По умолчанию лимит равен 5000 элементам. Если при выборе данных превысить его, то SharePoint выбросит исключение SPQueryThrottledException. И постраничная выборка в этом случае не учитывается. Для наглядности это можно представить в виде вот такой картинки.

В таком случае остается либо просить администратора приподнять эту планку, разрешив выбор большого количества данных либо использовать ContentIterator.

Но ContentIterator в SharePoint работает с объектами типа SPListItem, что делает невозможным применения модели данные, ориентированной на использование Linq to SharePoint.

Обход ограничения

Чтобы обойти это ограничение надо обрабатывать элементы пакетами, размер каждого из которых не будет превышать лимит. Ограничивать набор можно по полю ID. Оно есть всегда. Так же понадобятся минимальное и максимальное значения этого поля.

Мы будем перебирать элементы в цикле начиная с минимального значения поля ID, заканчивая максимальным, примерно вот так:

  1. var lastId = minId;
  2. for (var i = minId; i < maxId; i = i + throttleLimit)
  3. {
  4.     // Выполняем операции над элементами
  5. }

throttleLimit здесь - это максимально допустимое количество элементов, извлекаемое за один проход цикла. Получить это значение для веб-приложения можно из свойства SPWebApplication.MaxItemsPerThrottledOperation .

ContentIterator

Теперь перейдем к самому итератору. Для обеспечения гибкости в работе итератор должен также иметь следующие параметры:

  • Предикат для фильтрации данных. Его мы будем совмещать с ограничением по значению поля ID;
  • Путь к папке из которой необходимо выбирать данные. string.Empty в случае, когда данные надо выбрать из корневой папки;
  • Флаг, указывающий на необходимость выбора данных из дочерних папок;
  • Делегат для обработки элемента;
  • Делегат для обработки исключения при его возникновении во время обработки элемента;

Чтобы привязать экземпляр итератора к конкретному списку и типу данных, также будем передавать итератору репозиторий, описывающий данный список и тип данных. Учитывая вышесказанное конструктор для будущего итератора будет выглядеть вот так:

  1. public SPLinqContentIterator(BaseRepository<TEntity, DataContext> repository,
  2.     Expression<Func<TEntity, bool>> expression,
  3.     string path, bool recursive,
  4.     EntityProcessor entityProcessor,
  5.     EntityProcessorErrorCallout entityProcessorErrorCallout)
  6. {
  7.     //TODO
  8. }

Обработчик элемента будет возвращать флаг отмены операции для обеспечения возможности прервать итеративную обработку в любой момент:

  1. public delegate void EntityProcessor(TEntity entity, out bool isCancelled);

Обработчик для исключений выглядит аналогично:

  1. public delegate void EntityProcessorErrorCallout(TEntity entity, 
  2.                          Exception exception, out bool isCancelled);

Итеративный обход данных в списке/библиотеке документов будет проходить синхронно. Реализовать асинхронный итератор у меня не получилось, т.к. пачками летели гайзенбаги.

Теперь сам обработчик:

  1. private static void ContentIteratorWorker(
  2.     BaseRepository<TEntity, DataContext> repository,
  3.     Expression<Func<TEntity, bool>> expression,
  4.     string path, bool recursive,
  5.     int leftId, int rightId,
  6.     EntityProcessor entityProcessor,
  7.     EntityProcessorErrorCallout entityProcessorErrorCallout,
  8.     out bool cancelled)
  9. {
  10.     // Флаг отмены итерации опущен по умолчанию
  11.     cancelled = false;
  12.     // Получаем элементы, подлежащие обработке
  13.     var items = repository.GetEntityCollection(expression, path, recursive)
  14.         // Накладываем ограничение по полю ID
  15.         .Where(x => x.Id >= leftId && x.Id <= rightId)
  16.         .ToList();
  17.     foreach (var item in items)
  18.     {
  19.         // Если обработчик указан
  20.         if (entityProcessor != null)
  21.         {
  22.             try
  23.             {
  24.                 // Исполняем
  25.                 entityProcessor(item, out cancelled);
  26.                 // Сохраняем элемент
  27.                 repository.SaveEntity(item);
  28.             }
  29.             catch (Exception ex)
  30.             {
  31.                 // Если указан обработчик исключения, то исполняем его
  32.                 if (entityProcessorErrorCallout != null)
  33.                     entityProcessorErrorCallout(item, ex, out cancelled);
  34.             }
  35.         }
  36.         if (cancelled)
  37.         {
  38.             // Если флаг отмены поднят, то возвращаем управление
  39.             return;
  40.         }
  41.     }
  42. }

Теперь можно реализовать итеративную обработку данных, продолжая при этом использовать Linq to SharePoint.

Применение

В заключении я продемонстрирую простоту применения этого итератора.

Для обработки списка Сотрудников (описание которого здесь) используя стандартный механизм SharePoint надо написать примерно вот такой код:

  1. using (var site = new SPSite("http://sharepoint"))
  2. {
  3.     using (var web = site.OpenWeb())
  4.     {
  5.         var list = web.Lists["Employees"];
  6.         var query = new SPQuery
  7.         {
  8.             Query = @"<Where><Eq><FieldRef Name='Sex' /><Value Type='Choice'>Male</Value></Eq></Where>"
  9.         };
  10.         var nativeIterator = new ContentIterator();
  11.         nativeIterator.ProcessListItems(list, query, ProcessListItem, null);
  12.     }
  13. }
  14.  
  15. public static void ProcessListItem(SPListItem employee)
  16. {
  17.     employee["AccessLevel"] = ((int)(employee["AccessLevel"] ?? 0)) + 1;
  18.     employee.Update();
  19. }

Этот обработчик проходит по отфильтрованным элементам списка и изменяет значение поля AccessLevel. Аналогичная операция с использованием описанного здесь итератора будет выглядеть вот так:

  1. var repository = new EmployeeRepository("http://sharepoint"truefalse);
  2. var iterator = new SPLinqContentIterator<Employee>(
  3.                     repository, // Репозиторий
  4.                     emp => emp.SexValue == "Male"// Предикат для фильтрации
  5.                     string.Empty, // Корневая папка
  6.                     true// Просматривать дочерние папки
  7.                     ProcessEmployee, // Обработчик элемента
  8.                     null); // Обработчик исключений
  9. iterator.Begin();
  10.  
  11. public static void ProcessEmployee(Employee employee, out bool cancelled)
  12. {
  13.     employee.AccessLevel = employee.AccessLevel + 1;
  14.     cancelled = false;
  15. }

Во втором случае код читается гораздо легче, обработчик принимает объект конкретного типа, а не SPListItem. К тому же этот итератор можно инкапсулировать в метод расширитель для типа EntityList&lst;T>, что-то вроде repository.GetEntityCollection().ForEach(...).

Производительность

Я проделал данные итеративные обработки для списка сотрудников, содержащего около 15К элементов, подстраивая выборку так, чтобы в результирующем наборе было разное количество элементов. И результат у меня получился вот такой:

В случае с ContentIterator'ом на обработку одного элемента уходило около 10 секунд, а в случае с SPLinqContentIterator'ом - около 3 секунд. Если доступ к данным списков SharePoint обеспечивает провайдер Linq to SharePoint, то реализовывать итеративную обработку элементов лучше, не отходя от Linq to SharePoint. Получается и быстрее и дешевле при дальнейшей поддержке.

Для случаев, когда необходимо итеративно обработать другие элементы (SPWeb, SPFolder и прочее) ContentIterator остается незаменимым.

Исходные коды

Обновленный проект можно скачать с CodePlex. Код класса SPLinqContentIterator можно посмотреть здесь.

Виталий Жуков

Виталий Жуков

Техлид, Архитектор, Разработчик, Microsoft MVP. Более 20 лет опыта в области системной интеграции и разработки программного обеспечения. Специализируюсь на проектировании и внедрении масштабируемых высокопроизводительных программных решений в различных отраслях.

Смотрите также

Развертывание списков и библиотек с помощью SPFx-решений

Развертывание списков и библиотек с помощью SPFx-решений

SharePoint. Drag-and-Drop Загрузчик файлов

SharePoint. Drag-and-Drop Загрузчик файлов

CSOM. Загрузка файлов

CSOM. Загрузка файлов

SharePoint List REST API. Часть 2

SharePoint List REST API. Часть 2

SharePoint Framework. Создание веб-части на Angular

SharePoint Framework. Создание веб-части на Angular

SharePoint List REST API. Часть 1

SharePoint List REST API. Часть 1

Презентация с доклада о SharePoint Framework

Презентация с доклада о SharePoint Framework

SharePoint Framework. Создаем AngularJS 1.x Client WebPart

SharePoint Framework. Создаем AngularJS 1.x Client WebPart

SharePoint. Регистрация CSS и JavaScript с помощью DelegateControl

SharePoint. Регистрация CSS и JavaScript с помощью DelegateControl

SharePoint. Расширяем REST API

SharePoint. Расширяем REST API

SharePoint Excel Services. Создаем кредитный калькулятор

SharePoint Excel Services. Создаем кредитный калькулятор

SharePoint Ribbon API. Использование ToggleButton

SharePoint Ribbon API. Использование ToggleButton

SharePoint 2013. How To: настройка входящей почты для разработчиков

SharePoint 2013. How To: настройка входящей почты для разработчиков

Мифы и правда о Linq to SharePoint

Мифы и правда о Linq to SharePoint

5 особенностей SPSiteDataQuery

5 особенностей SPSiteDataQuery

SharePoint 2013. Введение в SharePoint App. Часть 2

SharePoint 2013. Введение в SharePoint App. Часть 2

SharePoint 2013. Введение в SharePoint App. Часть 1

SharePoint 2013. Введение в SharePoint App. Часть 1

Превью для веб-части в SharePoint 2010/2013

Превью для веб-части в SharePoint 2010/2013

SharePoint 2013. Еще немного о новых контролах

SharePoint 2013. Еще немного о новых контролах

SharePoint 2013. Контрол ClientPeoplePicker

SharePoint 2013. Контрол ClientPeoplePicker

SharePoint 2013. Контрол ImageCrop

SharePoint 2013. Контрол ImageCrop

SharePoint 2013. Тип поля Geolocation

SharePoint 2013. Тип поля Geolocation

Создание типа поля в SharePoint

Создание типа поля в SharePoint

SharePoint 2010. Длительные операции с обновляемым статусом

SharePoint 2010. Длительные операции с обновляемым статусом

Linq to SharePoint. Получение данных из другой коллекции сайтов

Linq to SharePoint. Получение данных из другой коллекции сайтов

Linq to SharePoint. Версионность

Linq to SharePoint. Версионность

SharePoint. Получение URL-адреса иконки для документа

SharePoint. Получение URL-адреса иконки для документа

SharePoint 2010. PostBack для Fluent Ribbon API

SharePoint 2010. PostBack для Fluent Ribbon API

Linq to SharePoint. Блокировка документов

Linq to SharePoint. Блокировка документов

Linq to SharePoint. Паттерн Repository

Linq to SharePoint. Паттерн Repository

Linq to SharePoint. Получение мета-данных списка

Linq to SharePoint. Получение мета-данных списка

Linq to SharePoint. Мапинг полей

Linq to SharePoint. Мапинг полей

Linq to SharePoint. Формирование данных для ProcessBatchData

Linq to SharePoint. Формирование данных для ProcessBatchData

Linq to SharePoint. Сравнение производительности с Camlex.NET

Linq to SharePoint. Сравнение производительности с Camlex.NET

Linq to SharePoint. Часть 5. Поля Choice и MultiChoice

Linq to SharePoint. Часть 5. Поля Choice и MultiChoice

Linq to SharePoint. Часть 4. Dynamic LINQ

Linq to SharePoint. Часть 4. Dynamic LINQ

Linq to SharePoint. Особенности. Часть 3

Linq to SharePoint. Особенности. Часть 3

Linq to SharePoint. Особенности. Часть 2

Linq to SharePoint. Особенности. Часть 2

SharePoint 2010. PeopleEditor. Установка значения

SharePoint 2010. PeopleEditor. Установка значения

SharePoint 2010. Настройка входящей почты для кастомного списка

SharePoint 2010. Настройка входящей почты для кастомного списка

Linq to Sharepoint. Особенности

Linq to Sharepoint. Особенности

EntityFramework. Оптимистические блокировки

EntityFramework. Оптимистические блокировки