Выборка элементов списка (Query / LINQ-стиль)
Как получить нужные элементы списка из WebPart, Event Receiver или Timer Job через fluent-API, похожий на LINQ.
Рекомендуемый путь: QueryListItemsAsync + ListItemQueryBuilder → IReadOnlyList<PortalListItem>.
Фильтр и сортировка уходят на сервер (push-down в PostgreSQL), без загрузки всего списка в память.
using Portal.Contracts.Lists;using Portal.WebPart.Sdk; // или EventReceiver.Sdk / TimerJob.Sdk
var items = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("Новая")) .OrderBy("updated_at", desc: true) .Take(50) .Build(), ct);| API | Результат |
|---|---|
QueryListItemsAsync | IReadOnlyList<PortalListItem> — предпочтительно |
QueryItemsAsync | сырой JSON (JsonElement / object) |
GetListItemsAsync / GetItemsAsync | первая страница представления без явного фильтра |
Колонки в фильтре — internal_name поля ("title", "status") или meta: __created_at, __updated_at.
Операторы: Eq, Ne, Contains, StartsWith, Gt / Gte / Lt / Lte, IsEmpty, IsNotEmpty.
После выборки обычный System.Linq по уже загруженным PortalListItem допустим для мелкой постобработки; тяжёлую фильтрацию держите в builder.
Справочник REST-фильтра: API списков. Значения полей: list-field-values.
1. Найти один элемент по ключу
Заголовок раздела «1. Найти один элемент по ключу»Типичный upsert: «есть ли запись с таким title / inn».
var found = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("title", f => f.Eq("GlobalSettings")) .Take(1) .Build(), ct);
var item = found.FirstOrDefault();if (item is null){ item = await ctx.Lists.NewListItemAsync(listId, ct); item["title"] = "GlobalSettings"; await item.CreateAsync(ct);}2. Фильтр по статусу / choice
Заголовок раздела «2. Фильтр по статусу / choice»var open = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("Новая")) .OrderBy("__created_at", desc: true) .Take(100) .Build(), ct);Несколько условий AND:
var items = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .Where(b => b .And() .Eq("status", "В работе") .Eq("priority", "Высокий")) .Take(50) .Build(), ct);OR (любой из статусов):
var items = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .Where(b => b .Or() .Eq("status", "Новая") .Eq("status", "В работе")) .Take(100) .Build(), ct);3. Поиск по тексту: Contains / StartsWith / Search
Заголовок раздела «3. Поиск по тексту: Contains / StartsWith / Search»// Подстрока в полеvar byTitle = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("title", f => f.Contains("отчёт")) .Take(30) .Build(), ct);
// Префиксvar byCode = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("code", f => f.StartsWith("HD-")) .Take(30) .Build(), ct);
// Общий поиск `q` (OR по текстовым полям, без учёта регистра)var searched = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .Search("переговорная") .Take(30) .Build(), ct);4. Диапазон дат и «просроченные»
Заголовок раздела «4. Диапазон дат и «просроченные»»Даты в фильтре — строки YYYY-MM-DD (поле date) или ISO для datetime.
var today = DateOnly.FromDateTime(DateTime.Today).ToString("yyyy-MM-dd");
// Срок на сегодня и раньше, ещё не закрытыvar overdue = await api.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .Where(b => b .And() .Lte("due_at", today) .Ne("status", "Готово") .IsNotEmpty("due_at")) .OrderBy("due_at") .Take(200) .Build(), ct);Плейсхолдеры представлений (как в UI фильтров):
// due_at <= сегодня.WhereField("due_at", f => f.Lte("[Сегодня]"))
// due_at в ближайшие 7 дней.Where(b => b.And().Gte("due_at", "[Сегодня]").Lte("due_at", "[Сегодня]+7"))5. Пустые / заполненные поля
Заголовок раздела «5. Пустые / заполненные поля»var withoutAssignee = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("assignee", f => f.IsEmpty()) .Take(50) .Build(), ct);
var withRoom = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("room", f => f.IsNotEmpty()) .Take(50) .Build(), ct);6. Person: «мои» записи и конкретный пользователь
Заголовок раздела «6. Person: «мои» записи и конкретный пользователь»Плейсхолдер [Я] — текущий пользователь контекста API (в WebPart / ER — пользователь запроса; в Timer Job — системная учётка, для «моих» там обычно не подходит).
// Назначенные на текущего пользователяvar mine = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("assignee", f => f.Eq("[Я]")) .OrderBy("updated_at", desc: true) .Take(50) .Build(), ct);
// Конкретный user id (UUID)var forUser = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("assignee", f => f.Eq(userId.ToString())) .Take(50) .Build(), ct);Часть сценариев по person / lookup может идти через in-memory fallback — не завышайте Take без нужды.
7. Lookup: по связанному элементу
Заголовок раздела «7. Lookup: по связанному элементу»В фильтре обычно сравнивают itemId связанного элемента (номер или UUID — как хранится в значении).
var roomItemId = "12"; // или guid
var bookings = await ctx.Lists.QueryListItemsAsync( bookingsListId, ListItemQueryBuilder.ForList(bookingsListId) .WhereField("room", f => f.Eq(roomItemId)) .OrderBy("start_at") .Take(100) .Build(), ct);8. Сортировка, страница, представление
Заголовок раздела «8. Сортировка, страница, представление»var page2 = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .View(viewId) // опционально: база из представления .WhereField("status", f => f.Ne("Архив")) // доп. фильтр AND к view .OrderBy("priority") .OrderBy("__updated_at", desc: true) // несколько OrderBy — по порядку .Page(2) .Take(50) .Build(), ct);| Метод | Назначение |
|---|---|
View(viewId) | стартовать с фильтра/сортировки представления |
OrderBy(column, desc?) | сортировка (internal_name или __created_at / __updated_at) |
Page(n) | номер страницы (с 1) |
Take(n) | размер страницы (limit) |
Search(q) | текстовый поиск |
9. Пакетная обработка (Timer Job)
Заголовок раздела «9. Пакетная обработка (Timer Job)»Не грузите «весь список» одним запросом — листайте страницами:
const int pageSize = 200;var page = 1;var updated = 0;
while (true){ var batch = await api.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("Новая")) .OrderBy("__created_at") .Page(page) .Take(pageSize) .Build(), ct);
if (batch.Count == 0) break;
foreach (var item in batch) { item["status"] = "В работе"; await item.UpdateAsync(ct); updated++; }
if (batch.Count < pageSize) break; page++;}
return new { updated };Id списка в Timer Job — из context.Config, не из Host.
10. Постобработка System.Linq (после выборки)
Заголовок раздела «10. Постобработка System.Linq (после выборки)»Когда условие неудобно выразить в фильтре списка — сузьте выборку builder’ом, затем дофильтруйте в памяти:
var items = await ctx.Lists.QueryListItemsAsync( listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("В работе")) .Take(200) .Build(), ct);
var highPriorityMine = items .Where(i => i.GetString("priority") == "Высокий") .Where(i => i.GetPerson("assignee").Any(p => p.Id == ctx.User!.Id.ToString())) .OrderBy(i => i.GetDate("due_at")) .ToList();11. Сырой JSON-фильтр (in и сложные случаи)
Заголовок раздела «11. Сырой JSON-фильтр (in и сложные случаи)»Оператор in и произвольный JSON удобны через ListItemQuery / ViewFilterJson (в fluent-builder отдельного In(...) нет):
var query = new ListItemQuery{ Filter = ViewFilterJson.Parse(""" { "logic": "and", "conditions": [ { "column": "status", "operator": "in", "value": ["Новая", "В работе"] } ] } """), Limit = 100, Sort = """[{"column":"updated_at","direction":"desc"}]""",};
var items = await ctx.Lists.QueryListItemsAsync(listId, query, ct);// или QueryItemsAsync для сырого JSONЭквивалент REST: GET /lists/{listId}/items?filter=...&limit=100.
Шпаргалка операторов
Заголовок раздела «Шпаргалка операторов»| Метод | Оператор | Пример |
|---|---|---|
Eq | равно | .Eq("status", "Новая") |
Ne | не равно | .Ne("status", "Архив") |
Contains | содержит | .Contains("title", "отчёт") |
StartsWith | начинается с | .StartsWith("code", "HD-") |
Gt / Gte / Lt / Lte | сравнение | .Lte("due_at", today) |
IsEmpty / IsNotEmpty | пусто / не пусто | .IsEmpty("assignee") |
| плейсхолдеры | — | "[Я]", "[Сегодня]", "[Сегодня]+7" |
Где ещё смотреть
Заголовок раздела «Где ещё смотреть»| Тема | Документ |
|---|---|
PortalListItem, запись полей | Кастомная форма |
| WebPart SDK | webparts/sdk § Запрос элементов |
| Timer Job | timer-jobs/sdk |
| Event Receiver | event-receivers/sdk |
| REST filter | api/lists |
| Сценарии модулей | modules/use-cases |