Перейти к содержимому

Выборка элементов списка (Query / LINQ-стиль)

Как получить нужные элементы списка из WebPart, Event Receiver или Timer Job через fluent-API, похожий на LINQ.

Рекомендуемый путь: QueryListItemsAsync + ListItemQueryBuilderIReadOnlyList<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Результат
QueryListItemsAsyncIReadOnlyList<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.


Типичный 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);
}

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);

// Подстрока в поле
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);

Даты в фильтре — строки 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"))

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: «мои» записи и конкретный пользователь»

Плейсхолдер [Я] — текущий пользователь контекста APIWebPart / 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 без нужды.


В фильтре обычно сравнивают 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);

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)текстовый поиск

Не грузите «весь список» одним запросом — листайте страницами:

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.


Когда условие неудобно выразить в фильтре списка — сузьте выборку 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();

Оператор 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 SDKwebparts/sdk § Запрос элементов
Timer Jobtimer-jobs/sdk
Event Receiverevent-receivers/sdk
REST filterapi/lists
Сценарии модулейmodules/use-cases