Обработчики и действия
Обработчик — это класс, помеченный [Handler(name)] и унаследованный от HandlerBase.
Его публичные методы с атрибутом [Action(name)] являются действиями. Клиент вызывает
действие по составному имени handler:action.
using DarkWS;
[Handler("message")]
public sealed class MessageHandler(MessageService messages) : HandlerBase {
[Action("get")]
public async Task<IResponse> GetAsync(Guid id) {
var message = await messages.FindAsync(id, ConnectionAborted);
return message is null ? Error("message:not-found") : Ok(message);
}
[Action("count")]
public IResponse Count() => Ok(messages.Count);
}
Регистрация
Регистрируйте обработчики при добавлении DarkWS:
builder.Services
.AddDarkWs()
.AddHandlersFromAssemblyContaining<Program>()
.AddHandlersFromAssembly(typeof(ExternalHandler).Assembly);
При регистрации сборка сканируется на неабстрактные классы, унаследованные от HandlerBase,
и каждое действие проверяется. Некорректные объявления приводят к ошибке при запуске:
InvalidOperationException с указанием типа, метода и причины.
- Вызывайте
AddDarkWs()один раз на коллекцию сервисов; повторный вызов выбрасывает исключение. Используйте возвращённыйDarkWsBuilderдля добавления других сборок, аутентификатора и инициализаторов областей. - Два действия с одинаковым составным именем приводят к ошибке регистрации. Обработчик без
[Handler]публикует свои действия под одним лишь именем действия. - Действия должны быть публичными методами экземпляра, объявленными в сканируемом классе.
Унаследованные методы не сканируются: объявите или переопределите действие в конкретном
обработчике и пометьте его
[Action]. - Действие возвращает строго
IResponseилиTask<IResponse>. - Действие принимает ноль или один параметр payload. Обобщённые методы, а также параметры, передаваемые по ссылке, byref-like-параметры и указатели отклоняются.
- Сканирование сборки также регистрирует каждый неабстрактный наследник
DarkWsMiddleware(см. Middleware). Инициализаторы областей регистрируются явно черезAddScopeInitializer<T>().
Зависимости и область
Каждый запрос выполняется в новой асинхронной области DI. Обработчик создаётся из этой
области, поэтому внедрение через конструктор scoped-сервисов, например DbContext, безопасно.
Область освобождается, когда действие возвращает результат, — до записи ответа.
Внутри действия HandlerBase предоставляет контекст запроса:
| Член | Значение |
|---|---|
Connection | Текущее IWebSocketConnection (Id, Session, IsOpen, HttpContext, SendAsync, CloseAsync) |
Session | Текущая сессия соединения; для анонимного соединения выбрасывает исключение |
HttpContext | HttpContext upgrade-запроса, общий для всего соединения |
AspNetSession | ASP.NET ISession, если установлен middleware сессий, иначе null |
ConnectionAborted | Отменяется при остановке соединения; передавайте его в асинхронную работу |
HandlerBase также предоставляет методы рассылки BroadcastAsync,
BroadcastToSessionAsync, BroadcastToGroupAsync и BroadcastToSelfAsync для
вызывающего соединения; см. Рассылки.
Наследуйтесь от HandlerBase<TSession>, чтобы получить типизированный Session; см.
Аутентификация и сессии.
Сервисы вне обработчиков могут внедрить IDarkWsContextAccessor для доступа к тем же данным;
см. Инициализаторы областей и middleware.
Payload
Поле data запроса десериализуется в параметр действия с помощью
DarkWsOptions.JsonOptions (веб-умолчания: camelCase, без учёта регистра).
- Nullable-параметр (
string?,Input?,int?) принимает отсутствующий или null payload. - Non-nullable-параметр требует payload. Если данные опущены или равны null, возвращается
darkws:error:invalid-request, а обработчик не вызывается. - Несоответствие типов и числовое переполнение в
dataвозвращаютdarkws:error:invalid-requestещё до создания обработчика. - На сообщение, которое не является корректным JSON или не содержит
idилиaction, отвечаетсяdarkws:error:invalid-request, только если удаётся прочитать егоid; иначе ответа нет. - Используйте
JsonElement, чтобы принимать произвольный JSON.
Результаты
Возвращайте один из хелперов:
| Хелпер | Ответ в протоколе |
|---|---|
Ok() | { "id": "…" } |
Ok(value) | { "id": "…", "data": value } |
Error("code") | { "id": "…", "error": "code" } |
Error("code", details) | { "id": "…", "error": "code", "data": details } |
Коды ошибок не должны быть пустыми. Используйте стабильные коды с пространством имён,
например message:not-found, по которым клиенты смогут ветвиться.
Результаты сериализуются после освобождения области сообщения. Материализуйте данные,
зависящие от scoped-сервисов (например, вызовите ToListAsync() у запроса), до того как
вернуть их. Результат null, значение, которое не удаётся сериализовать, или упавший
пользовательский IResponse логируются как предупреждение, и клиент не получает ответа
на этот запрос. Соединение остаётся открытым.
Чтобы написать собственный ответ, реализуйте IResponse.WriteResultAsync(ResponseContext, CancellationToken)
и отправляйте данные через context.SendAsync(data).
Ошибки
Выбросьте ErrorResponseException из любого места цепочки вызовов, чтобы вернуть
контролируемую ошибку:
if (!await permissions.CanEditAsync(Session.UserId, id)) {
throw new ErrorResponseException("message:forbidden");
}
throw new ErrorResponseException<ValidationDetails>("message:invalid", details);
Любое другое исключение логируется как предупреждение, и на него отвечается
darkws:error:request-failed. Его сообщение никогда не отправляется клиенту. Чтобы создать
собственный тип исключения с ответом протокола, унаследуйтесь от DarkWsException и
реализуйте GetResponse().
Встроенные коды ошибок перечислены в разделе Протокол.
Авторизация
По умолчанию обработчики требуют аутентифицированной сессии. Пометьте обработчик или
отдельное действие [AllowAnonymous], чтобы сделать его публичным:
[Handler("system"), AllowAnonymous]
public sealed class SystemHandler : HandlerBase {
[Action("ping")]
public IResponse Ping() => Ok(new { ServerTime = DateTimeOffset.UtcNow });
}
Вызов защищённого действия без сессии или с сессией, чей User не аутентифицирован,
возвращает darkws:error:authorization-required.
Встроенная авторизация различает только аутентифицированные и анонимные соединения.
[Authorize], атрибуты ролей и политик и любые другие IAuthorizeData на обработчике
или действии приводят к ошибке регистрации, а не игнорируются молча. Проверяйте прикладные
разрешения в действии и возвращайте контролируемую ошибку при отказе.
Параллелизм
Одновременно выполняется до MaxConcurrentRequestsPerConnection (16) действий одного
соединения, и ответы могут приходить не по порядку. Когда лимит достигнут, DarkWS
перестаёт читать сокет, пока не завершится какое-либо действие, поэтому ping, auth: и
logout тоже ждут. Параллельные действия разделяют HttpContext, Items, features и
AspNetSession соединения, которые не являются потокобезопасными:
- Храните состояние отдельного запроса в scoped-сервисах, а не в
HttpContext.Items. - Считывайте нужные действию данные из
HttpContextв самом его начале. - Сериализуйте запись в
ISessionсамостоятельно или установитеMaxConcurrentRequestsPerConnection = 1.
Действие запускается в цикле диспетчеризации соединения, поэтому синхронный обработчик
или часть асинхронного обработчика до первого await задерживает диспетчеризацию
следующего сообщения.
Состояние сессии ASP.NET
Чтобы использовать AspNetSession, зарегистрируйте AddSession() и вызовите UseSession() до
MapDarkWs(). WebSocket — это один длинный запрос, поэтому вызывайте
HttpContext.Session.CommitAsync(), когда изменение нужно сохранить немедленно.
Обработчики, переживающие соединение
При завершении работы DarkWS отменяет ConnectionAborted и ждёт до ShutdownTimeout.
.NET не может остановить код, игнорирующий отмену: такой обработчик удерживает свою область,
пока не завершится, а его ответ отбрасывается. После завершения работы upgrade-запрос
закончен, и ASP.NET Core может переиспользовать его HttpContext, поэтому не читайте
HttpContext, AspNetSession и Connection.HttpContext из такого обработчика. Скопируйте
нужные значения запроса до начала долгой работы; Session и его User остаются доступными
для чтения.