SDK (Portal.TimerJob.Sdk)
SDK (Portal.TimerJob.Sdk)
C# SDK для разработки Timer Job. Контракт: backend/src/Portal.TimerJob.Sdk/ITimerJob.cs.
Runtime Worker инжектирует ITimerJobApi — см. backend/src/Portal.Application/TimerJobs/TimerJobApiHost.cs.
Интерфейс обработчика
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 не получает прямой доступ к спискам и узлам через SDK. Для работы с данными портала ставьте задания в очередь (api.Jobs.EnqueueAsync) или выносите логику во встроенные job-обработчики.
TimerJobContext
| Поле | Тип | Описание |
|---|---|---|
TimerJobId | Guid | ID экземпляра из timer_jobs |
JobType | string | manifest.id (например contoso.heartbeat) |
Config | JsonElement | Параметры экземпляра из админки (timer_jobs.config) |
ScheduledAt | DateTimeOffset | Время планового (или ручного) запуска |
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— срабатывание cron-планировщика
Timer Job API (ITimerJobApi)
Логирование (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)
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 объединяются с этими служебными полями.
Типичный паттерн: Timer Job по расписанию ставит пакетное задание (например, через Event Receiver job type или встроенный handler), а тяжёлая работа выполняется асинхронно в очереди.
Асинхронное выполнение и отмена
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 и не бросайте исключение.
Шаблон проекта
cd backenddotnet new install ./templates/portal-timerjobdotnet new portal-timerjob -n MyTimerJob -o ../../packages/my-timerjobПример: packages/timer-job-examples/heartbeat.
Ограничения SDK
| Доступно | Недоступно напрямую |
|---|---|
api.Log | api.Lists (как у Event Receiver) |
api.Jobs.EnqueueAsync | api.Nodes |
Чтение context.Config | HTTP-запросы к внешним API (делайте сами в C#) |
Если нужен прямой доступ к спискам по расписанию — рассмотрите цепочку: Timer Job → EnqueueAsync → Event Receiver / custom job handler с IReceiverApi.