Перейти до основного вмісту

Надсилання сповіщень

Скористайтесь вбудованим skill

Віддавайте перевагу skill send-notifications (у .claude/skills/ вашого згенерованого репозиторію) замість ручної реалізації. Це чек-лист-версія цієї сторінки.

Сповіщення — це повідомлення, що потрапляє до вхідних користувача: іконка дзвіночка з лічильником непрочитаних у верхній панелі та окрема сторінка Сповіщень; якщо користувач онлайн, воно також одразу надсилається в браузер. Конфігураційний код надсилає його одним викликом з Обробника даних або з behavior документа: "імпорт завершено", "3 документи потребують перевірки".

Сповіщення — це збережений рядок; живе надсилання лише доповнення. Користувач, який був офлайн, побачить його при наступному вході. Це не новий вид сутності — немає ні Dto, ні атрибута, ні міграції, ні реєстрації в DI. Ви ін'єктуєте один сервіс і викликаєте його.

Сервіс​

INotificationService — звичайний scoped-сервіс, тож його можна ін'єктувати через конструктор будь-де на сервері. Це свідомо відрізняється від записувачів регістрів і сервісу проведення, які доступні лише через ISubmitScope, що документ передає в OnSubmitAsync — надсилання повідомлення не є операцією проведення.

МетодОдержувач
NotifyCallerAsyncТой, хто ініціював поточну роботу — звичайний вибір (див. розділ про два режими запуску нижче)
NotifyUserAsync / NotifyUsersAsyncОдин або кілька названих користувачів
NotifyRoleAsyncУчасники ролі (роль має дозволити це, див. нижче)
NotifyWithPermissionAsyncУсі, хто має певний дозвіл
NotifyAdminsAsyncУсі, кого рушій вважає адміністраторами

Кожен метод приймає те саме значення Notification, яке каже, що повідомляється, і ніколи — хто це почує, тож те саме повідомлення можна надіслати і користувачеві, і ролі. Метод повертає кількість одержувачів, яким воно було адресовано.

Notification має Title, необов'язковий Body, Severity (Info, Success, Warning, Error), необов'язкові Links та необов'язковий DeduplicationKey. Текст — це NotificationText: NotificationText.ForKey("Some_Key", arg0, arg1) — ключ ресурсу локалізації, що перетворюється на текст мовою читача в момент відкриття (додайте ключ до кожного resx-файлу вашого проєкту локалізації, з плейсхолдерами на кшталт {0}), тоді як NotificationText.ForLiteral("...") зберігається як є й показується всім однією мовою. Надавайте перевагу ключам.

Наскрізний приклад: повідомити, коли імпорт курсів НБУ завершено​

Обробник даних ImportNbuRates еталонної конфігурації (той самий, який сторінка Планувальник задач запускає щоранку) — перший продюсер. Після створення документа RateImport він завершується так:

// Acme.Application/DataProcessors/ImportNbuRates.cs
using NotificationMessage = Kandra.Application.Abstractions.Services.Notifications.Notification;

public class ImportNbuRatesBehavior(
/* ... */
INotificationService notifications,
IUnitOfWork unitOfWork)
: IDataProcessorBehavior<ImportNbuRatesDto, ImportNbuRatesResult>
{
public async Task<ImportNbuRatesResult> OnProcessAsync(ImportNbuRatesDto input, CancellationToken cancellationToken)
{
// ... отримати курси, побудувати та вставити RateImport як `created` ...

await notifications.NotifyCallerAsync(new NotificationMessage(
Title: NotificationText.ForKey("NbuRatesImport_Done", dto.Lines.Count, dto.Code),
Severity: NotificationSeverity.Success,
Links: [new NotificationLink
{
Kind = NotificationLinkKind.Document,
EntityTypeId = RateImport.TypeId,
EntityId = created.Id,
CaptionSnapshot = $"Rate Import {dto.Code}",
}]), cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);

return new ImportNbuRatesResult { /* ... */ };
}
}

Псевдонім using ... = ...Notification потрібен, бо запис Notification рушія збігається за назвою з іншими типами, які ймовірно є у вашій області видимості. Із resx-записом NbuRatesImport_Done = Imported {0} rate(s) into {1}. (та його українським і російським варіантами) кожен читач бачить заголовок своєю мовою, із посиланням, що відкриває створений документ.

Дві деталі, які варто перейняти: повідомлення надсилається після успішного виконання роботи (запуск, що завершився помилкою, ніколи не оголошує успіх), і це одне зведення на запуск, а не по одному на кожен імпортований рядок.

Один виклик, два режими запуску​

Один і той самий обробник можна запустити двома способами: людина натискає Execute, або Планувальник задач запускає його без нагляду від імені користувача Run As цієї задачі. Код продюсера вище однаковий в обох випадках, і кожен раз сповіщення потрапляє до потрібної людини:

  • Ручний запуск — NotifyCallerAsync через ICallerContext визначає користувача, що увійшов.
  • Запланований запуск — планувальник встановив перевизначення виклику на користувача Run As задачі, тож той самий виклик сповіщає його, а не того, хто випадково створив задачу.

Тому не розгалужуйте код за принципом "чи я задача?" і не прописуйте одержувача жорстко заради запланованого випадку — це зламає інший шлях. Викликайте NotifyCallerAsync і дозвольте ICallerContext вирішити. (Якщо ви хочете, щоб адміністратор дізнався в будь-якому разі, додайте окремий NotifyAdminsAsync.)

Писати сповіщення "я завершився помилкою" для запланованих обробників не потрібно: планувальник сам сповіщає адміністраторів і користувача Run As, коли задача завершується помилкою (раз на серію збоїв, із 24-годинною паузою), і надсилає одне повідомлення "відновилось", коли вона знову успішна. Достатньо, щоб обробник кинув виняток.

Збереження: сервіс готує, ви фіксуєте​

INotificationService ніколи не зберігає. Кожен виклик додає рядки до поточної одиниці роботи (unit of work), завдяки чому повідомлення атомарне з роботою, яку воно описує:

  • У хуку behavior документа (OnSubmitAsync, OnSaveAsync, ...) власне збереження документа фіксує сповіщення разом із документом. Нічого додатково робити не потрібно.
  • В Обробнику даних викличте await unitOfWork.SaveAsync(cancellationToken) після сповіщення, як у прикладі. Якщо забути, підготовлені рядки мовчки відкидаються після завершення scope запиту — без помилки і без сповіщення.

Живе надсилання у відкриті браузери відбувається лише після успішної фіксації та є best-effort: збій надсилання логується і ніколи не провалює ваше збереження.

Надсилання ролі, за дозволом або адміністраторам​

Методи для аудиторії призначені для "комусь потрібно на це подивитись", а не "скажи одній людині":

await notifications.NotifyAdminsAsync(msg, cancellationToken: ct);
await notifications.NotifyRoleAsync("Accountants", msg, cancellationToken: ct);
await notifications.NotifyWithPermissionAsync(new SomeAuthorizationPoint(), msg, cancellationToken: ct);

Три запобіжники не дають їм перетворитися на спам-гармату:

  • Ролі дають згоду. NotifyRoleAsync кидає виняток, якщо в сторінці адміністрування ролей не ввімкнено перемикач Може отримувати групові сповіщення. Якщо роль існує, але учасників немає, записується попередження і нікому нічого не надсилається. Ролі зіставляються за назвою.
  • Обмеження розсилки. Понад 50 одержувачів — виняток. Передайте new AudienceOptions { MaxRecipients = 200 }, коли це свідомо.
  • Відправник виключається зі своєї ж розсилки, якщо не задати IncludeSender = true.

Посилання​

Links додають до сповіщення клікабельні посилання. Існує два види:

KindЗадаєтьсяВідкриває
DocumentEntityTypeId (TypeId документа) та EntityIdсторінку документа
JobRunEntityId (ідентифікатор запланованого запуску)історію запусків планувальника

CaptionSnapshot — текст посилання, і ви пишете його в момент надсилання. Це свідомо копія, а не пошук, тож посилання читається правильно, навіть якщо ціль пізніше перейменують чи видалять. Виду посилання на довідник наразі немає; посилайтесь на документ або вкажіть назву в заголовку.

Ідемпотентні продюсери: DeduplicationKey​

Обробник, що багаторазово запускається над тими самими фактами — скажімо, запланована перевірка рахунків без накладної — інакше нагадував би про той самий рахунок щоразу. Надайте кожному логічному повідомленню стабільний ключ:

new NotificationMessage(
Title: NotificationText.ForKey("Invoice_NoWaybill", invoice.Code),
Severity: NotificationSeverity.Warning,
DeduplicationKey: $"hint:InvoiceWithoutWaybill:{invoice.Id}")

Одержувач, у якого вже є сповіщення з таким ключем, мовчки пропускається, а виклик враховує лише тих, кого справді сповістили (можливо, 0). Ключ перевіряється для кожного одержувача окремо, і вже прочитане сповіщення все одно блокує повтор. Не вказуйте ключ для одноразових повідомлень "запуск завершено", які мають надходити завжди.

Перевірка​

  1. Запустіть обробник з його сторінки та простежте, як зростає лічильник дзвіночка. Відкрийте Сповіщення й перевірте локалізований заголовок та те, що посилання відкриває потрібний документ.
  2. Для запланованого обробника створіть задачу з користувачем Run As, відмінним від вас, і натисніть Trigger now: сповіщення надійде користувачеві Run As, а не вам.
  3. Якщо ви використали DeduplicationKey, запустіть двічі й переконайтесь, що сповіщення все ще одне.

Що ще не побудовано​

Щоб ви не планували на тому, чого немає:

  • Чату (розмови між користувачами та підтримка) не існує. Цю сторінку буде розширено, коли він з'явиться.
  • Підказки робочого процесу — запланований обробник, що нагадує про відсутні наступні кроки, — не потребують змін рушія (це саме шаблон DeduplicationKey вище), але готового прикладу в еталонній конфігурації поки немає.
  • Зберігання фіксоване. Сповіщення, які ви прочитали, видаляються через 90 днів (перевірка щошість годин); непрочитані зберігаються безстроково. Налаштування для окремої інсталяції немає.
  • Не місце для обробників (handlers). Handlers — це серверні запити посеред редагування; не надсилайте з них сповіщення.

Дивіться також​