SDK (Portal.TimerJob.Sdk)
C# SDK для разработки Timer Job.
Настройка SDK: Extension SDK с korport.ru/developers.
Интерфейс обработчика
Заголовок раздела «Интерфейс обработчика»using Portal.TimerJob.Sdk;
public sealed class ContosoHeartbeatTimerJob : ITimerJob{ public Task<object?> ExecuteAsync( TimerJobContext context, ITimerJobApi api, CancellationToken cancellationToken = default) { var message = context.Config.ValueKind == JsonValueKind.Object && context.Config.TryGetProperty("message", out var msgEl) ? msgEl.GetString() ?? "heartbeat" : "heartbeat";
api.Log.Info("Contoso heartbeat:", message, "job=", context.JobType, "timerJobId=", context.TimerJobId, "scheduledAt=", context.ScheduledAt);
return Task.FromResult<object?>(new { ok = true, message }); }}В отличие от Event Receiver, Timer Job не наследует пользователя события. Операции со списками и библиотеками всегда выполняются от системной учётки portal-system (администратор портала, полный доступ).
TimerJobContext
Заголовок раздела «TimerJobContext»| Поле | Тип | Описание |
|---|---|---|
TimerJobId | Guid | ID экземпляра из timer_jobs |
JobType | string | manifest.id (например contoso.heartbeat) |
Config | JsonElement | Параметры экземпляра из админки (timer_jobs.config) |
ScheduledAt | DateTimeOffset | Время планового (или ручного) запуска |
Payload задания в Worker
Заголовок раздела «Payload задания в Worker»При постановке в очередь Worker получает JSON:
{ "source": "timerJob", "timerJobId": "uuid-экземпляра", "jobType": "contoso.heartbeat", "scheduledAt": "2026-07-06T09:30:00.0000000Z", "manual": false, "config": { "message": "hello" }}manual: true— ручной запуск из админки (POST /timer-jobs/{id}/run)manual: false— срабатывание по расписанию
Timer Job API (ITimerJobApi)
Заголовок раздела «Timer Job API (ITimerJobApi)»| Объект | Методы |
|---|---|
Log | Info / Warn / Error (params object[]) |
Jobs | EnqueueAsync(serviceKey, jobType, jobPayload?) |
Lists | GetListItemAsync / NewListItemAsync / QueryListItemsAsync (PortalListItem); также GetAsync, GetFieldsAsync, GetItemAsync, GetItemVersionsAsync, GetItemVersionAsync, GetItemsAsync, QueryItemsAsync, CreateItemAsync, UpdateItemAsync, DeleteItemAsync. Нет «текущего элемента» как у WebPart ctx.Host / ER context.GetListItemAsync — id списка и фильтр берите из context.Config |
Libraries | GetAsync, GetFilesAsync, QueryFilesAsync |
Users | GetProfileAsync, GetOrgChainAsync, GetOrgChildrenAsync, ListActiveDirectoryUsersAsync, AvatarUrl |
Permissions | ForListAsync / ForListItemAsync / GrantToGroupAsync / RevokeAsync / … |
Kedo | CreatePackageAsync / GetPackageStatusAsync |
Логирование (api.Log)
Заголовок раздела «Логирование (api.Log)»api.Log.Info("Старт обработки", context.JobType);api.Log.Warn("Пропущена запись", itemId);api.Log.Error("Ошибка синхронизации:", ex.Message);Сообщения попадают в лог Worker с префиксом [timer-job]. Просмотр:
docker compose logs -f workerОчередь заданий (api.Jobs)
Заголовок раздела «Очередь заданий (api.Jobs)»Timer Job может поставить другое задание в portal_jobs:
var result = await api.Jobs.EnqueueAsync( serviceKey: "audit", jobType: "audit.logEvent", jobPayload: new { action = "timer.heartbeat", timerJobId = context.TimerJobId, message = "tick", }, cancellationToken);
// result: { id = Guid задания, status = "pending" }В payload автоматически добавляются:
{ "triggeredBy": "timerJob", "sourceTimerJobId": "uuid-экземпляра"}Поля из jobPayload объединяются с этими служебными полями.
Списки (api.Lists)
Заголовок раздела «Списки (api.Lists)»| Метод | Описание |
|---|---|
GetListItemAsync / NewListItemAsync / QueryListItemsAsync | SharePoint-подобный PortalListItem: item["title"], CreateAsync / UpdateAsync. Частые фильтры: list-queries |
GetAsync | Метаданные списка |
GetFieldsAsync | Схема полей |
GetItemAsync | Один элемент (сырой JSON) |
GetItemVersionsAsync | История версий (fieldValues в каждой версии) |
GetItemVersionAsync | Одна версия по номеру |
GetItemsAsync / QueryItemsAsync | Выборка элементов (сырой JSON) |
CreateItemAsync / UpdateItemAsync | Низкоуровневое создание / изменение |
Права — всегда от системной учётки portal-system.
Библиотеки (api.Libraries)
Заголовок раздела «Библиотеки (api.Libraries)»| Метод | Описание |
|---|---|
GetAsync | Метаданные библиотеки |
GetFilesAsync / QueryFilesAsync | Выборка файлов |
Также от имени portal-system.
В config экземпляра достаточно параметров задания (например settingsListId). Отдельный пользователь для выполнения не задаётся.
{ "settingsListId": "uuid-списка"}Пример — найти и обновить элемент настроек (PortalListItem):
using Portal.Contracts.Lists;using Portal.TimerJob.Sdk;
public async Task<object?> ExecuteAsync(TimerJobContext context, ITimerJobApi api, CancellationToken ct){ var listId = Guid.Parse(context.Config.GetProperty("settingsListId").GetString()!);
var items = await api.Lists.QueryListItemsAsync(listId, ListItemQueryBuilder.ForList(listId) .WhereField("title", f => f.Eq("GlobalSettings")) .Take(1) .Build(), ct);
var item = items.First(); var oldValue = item.GetString("config_value"); item["config_value"] = "новое значение"; await item.UpdateAsync(ct);
api.Log.Info("Updated settings", oldValue, "->", item.GetString("config_value")); return new { ok = true, oldValue };}Пример — один элемент по ref:
var item = await api.Lists.GetListItemAsync(listId, "12", ct); // PortalListItemvar title = item.GetString("title"); // string?item["status"] = "Обработано";await item.UpdateAsync(ct);Типы геттеров (GetDate, GetLookup, …): Кастомная форма §1.
Пример — выборка PDF из библиотеки:
using Portal.Contracts.Libraries;using Portal.TimerJob.Sdk;
var libraryId = context.Config.GetProperty("libraryId").GetString()!;
var files = await api.Libraries.QueryFilesAsync(libraryId, LibraryFileQueryBuilder.ForLibrary(Guid.Parse(libraryId)) .Where(b => b.Eq("item_type", "file").Eq("mime_type", "application/pdf")) .Take(100) .Build(), ct);Низкоуровневый UUID-путь и версии: Значения полей списков.
Типичный паттерн для тяжёлой работы: Timer Job по расписанию ставит пакетное задание (api.Jobs.EnqueueAsync), а обработка выполняется асинхронно в очереди.
Асинхронное выполнение и отмена
Заголовок раздела «Асинхронное выполнение и отмена»public async Task<object?> ExecuteAsync( TimerJobContext context, ITimerJobApi api, CancellationToken cancellationToken){ for (var i = 0; i < 100; i++) { cancellationToken.ThrowIfCancellationRequested(); await ProcessBatchAsync(i, cancellationToken); }
return new { processed = 100 };}При остановке Worker активные задания получают CancellationToken от хоста.
Возвращаемое значение
Заголовок раздела «Возвращаемое значение»ExecuteAsync возвращает object? — результат сохраняется в portal_jobs (поле результата задания). Используйте для диагностики:
return new { ok = true, processed = count, skipped = skipped };При необработанном исключении задание переходит в failed (до 3 попыток с интервалом 30 с). См. Job-модули.
Обработка ошибок
Заголовок раздела «Обработка ошибок»public async Task<object?> ExecuteAsync(TimerJobContext context, ITimerJobApi api, CancellationToken ct){ try { await DoWorkAsync(context, ct); return new { ok = true }; } catch (Exception ex) { api.Log.Error("Timer job failed:", ex.Message); throw; // задание будет повторено / помечено failed }}Для «мягких» ошибок (пропуск итерации без fail) верните объект с ok: false и не бросайте исключение.
Шаблон проекта
Заголовок раздела «Шаблон проекта»dotnet new install ./portal-sdk-1.0.0/templates/portal-timerjobdotnet new portal-timerjob -n MyTimerJob -o ./my-timerjobПример в SDK ZIP: examples/heartbeat. См. Extension SDK.
Ограничения SDK
Заголовок раздела «Ограничения SDK»| Доступно | Недоступно напрямую |
|---|---|
api.Log | api.Nodes |
api.Jobs.EnqueueAsync | HTTP к внешним API (реализуйте в C# сами) |
api.Lists.* / api.Libraries.* (всегда от portal-system) | — |
Если нужна реакция на изменение элемента в реальном времени — используйте Event Receiver вместо опроса списка по расписанию.