Создание контроллера веб-сервиса на основе API TESSA¶
Платформа TESSA позволяет добавлять собственные контроллеры для веб-сервиса. Эти контроллеры могут быть полезны при определённых требованиях, например, когда требуется предоставить специфичное API для обращения к нему сторонней системы или в целях оптимизации операций, для которых применение интерфейса ICardRepository
является излишним.
Примеры готовых контроллеров можно найти в проекте Tessa.Extensions.Default.Server.Web
в папке Controllers
и в проекте Tessa.Extensions.Server.Web
в папке Services
типового решения.
Собственные контроллеры необходимо описывать в проекте Tessa.Extensions.Server.Web
, либо в другом проекте, зависящем от nuget-пакета Tessa.Web
. Чтобы методы контроллеров из другого проекта были доступны, в конфигурационном файле app.json
веб-сервиса в настройку Settings
по ключу WebControllers
необходимо добавить название сборки. Например, если был добавлен проект с именем Tessa.MyWebServerProject
, а потом сборка была помещена в папку extensions
веб-сервиса, то app.json
должен содержать следующую настройку:
{
// ...
"Settings": {
// ...
"WebControllers": [
"extensions/Tessa.Extensions.Default.Server.Web.dll",
"extensions/Tessa.Extensions.Server.Web.dll",
"extensions/Tessa.MyWebServerProject.dll"
],
// ...
}
}
Далее будет подробно рассмотрено создание собственных REST-контроллеров, а также создание клиентского сервиса для осуществления HTTP-запросов к нему.
Создание контроллера¶
Создание класса контроллера рассмотрим на примере ServiceController
из проекта Tessa.Extensions.Server.Web
.
Объявление класса¶
/// <summary>
/// Контроллер, для которого задан базовый путь "service".
/// Является примером реализации сервисов в рамках приложения TESSA.
/// </summary>
/// <remarks>
/// ...
/// </remarks>
[Route("service"), AllowAnonymous, ApiController]
[ProducesErrorResponseType(typeof(PlainValidationResult))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public sealed class ServiceController :
Controller
{
// ...
}
Класс ServiceController
наследуется от абстрактного класса Controller
, который предоставляет методы для реагирования на HTTP-запросы. Наследование от него является обязательным и достаточным для того, чтоб контроллер начал функционировать. Дополнительная регистрация класса не требуется.
Note
Наследоваться можно не только от Controller
, но и от его родительского класса ControllerBase
. Подробнее о них можно прочитать в MSDN.
Warning
Обратите внимание, что имя класса ServiceController
имеет суффикс Controller
, что является следованием соглашению о наименовании. Поэтому, при написании собственных контроллеров, его также следует использовать, например MySpecialController
.
Разберём атрибуты:
-
[ApiController]
- добавляет контроллеру специализированное поведение, упрощающее разработку. Например, обязательная маршрутизация, автоматические ответы с HTTP-кодом 400 и прочее (подробнее см. в MSDN). Рекомендуется использовать данный атрибут. -
[Route(...)]
- атрибут для использования маршрутизации, называемой Attribute Routing. Является обязательным при использованииApiController
. В данном параметре задается базовый путь для всех методов контроллера (подробнее см. в MSDN). -
[AllowAnonymous]
- указывает, что контроллер не требует авторизации. -
[ProducesErrorResponseType(...)]
- создаёт описательные сведения о модели ошибки по умолчанию для клиентской ошибки (т.е. результата с одним из HTTP-кодов ошибки). Атрибут может быть размещён и в описании метода, но общие для всех методов значения атрибутов лучше размещать в описании класса. -
[ProducesResponseType(...)]
- создаёт описательные сведения о коде ошибки в ответе. Атрибут может быть размещён и в описании метода, но общие для всех методов значения атрибутов лучше размещать в описании класса.
Следовательно, маршрутом контроллера является service
, сам контроллер не требует авторизации, все его методы могут возвращать HTTP-коды ошибок 400, 401 и 403, а моделью ошибки является тип PlainValidationResult
.
Информация об атрибутах Route
, ProducesErrorResponseType
и ProducesResponseType
, а также описание класса, находящееся в специальном теге <summary>
, будет видна в Swagger. Чтобы проверить это, достаточно включить в конфигурационном файле app.json
веб-сервера настройку SwaggerDocIsEnabled
в разделе Settings
(подробнее см. в разделе Настройка конфигурационного файла), а потом перейти по ссылке https://<адрес сервиса>/swagger
. В результате можно увидеть следующее:
Объявление конструктора и получение зависимостей¶
Прежде, чем перейти к описанию самого конструктора, кратко рассмотрим возможности внедрения зависимостей через его параметры.
В контроллерах веб-сервисов TESSA может использоваться один из следующих DI-контейнеров:
-
Unity
- контейнер, содержащий все необходимые объекты для работы с API TESSA. -
Стандартный DI-контейнер ASP.NET Core в одном из следующих режимов:
-
DefaultControllerActivator
- получение только параметров конструктора происходит изIServiceProvider
; -
ServiceBasedControllerActivator
- получение самого контроллера происходит изIServiceProvider
.
-
Для выбора DI-контейнера, существует ещё один атрибут класса [TessaController(...)]
. В качестве его параметра ActivationMode
можно передать одно из следующих значений:
-
Unity
- получение зависимостей должно происходить в соответствии с активаторомTessaControllerActivator
, т.е. изUnity
-контейнера, а при отсутствии в нём, - из DI-контейнера ASP.NET Core черезIServiceProvider.GetRequiredService
; -
AspNetCore
- получение зависимостей должно происходить из DI-контейнера ASP.NET Core c активатором, установленным по умолчанию (DefaultControllerActivator
); -
ServiceBased
- получение самого контроллера с его зависимостями происходит с помощью вызова методаIServiceProvider.GetRequiredService
.
Tip
Если необходимо очищать ресурсы контроллера, можно добавить реализацию интерфейса IDisposable
, которая будет автоматически вызвана после завершения работы методов.
Note
Если у класса явно не указан атрибут TessaController
, то логика получения зависимостей и освобождения ресурсов соответствует атрибуту [TessaController(ActivationMode = Unity)]
. При этом у класса может быть объявлено несколько конструкторов. В таком случае будет выбран тот конструктор, который либо соответствует указанию InjectionConstructor
при явной регистрации контроллера в Unity
, либо имеет атрибут [InjectionConstructor]
, либо же имеет наибольшее количество параметров.
Рассмотрим несколько полезных атрибутов, предоставляемых API Unity
:
-
[Dependency(...)]
- используется для параметров конструктора и позволяет получить зависимость, которая зарегистрирована в контейнере под определенным именем. Существует альтернативный способ передачи в конструктор такой зависимости (через явную регистрацию класса с помощью указанияInjectionConstructor
), но указание данного атрибута является намного более предпочтительным. -
[OptionalDependency]
- помечает параметр как необязательный для получения зависимости. Если в контейнере данная зависимость отсутствует, в качестве значения параметра будет переданnull
. -
[InjectionConstructor]
- имеет смысл применять, когда в классе больше одного конструктора. Он позволяет определить, какой именно конструктор должен использоваться при создании экземпляра контроллера (подробнее см. руководство по API Unity).
У DI-контейнер ASP.NET Core есть аналог атрибута [Dependency]
, который также позволяет получать зависимость, зарегистрированную под определенным ключом (см. подробнее FromKeyedServices
).
Теперь рассмотрим описание конструктора:
public ServiceController(
ITessaServerSettings serverSettings,
ISessionServer sessionServer,
ISessionService sessionService,
IDbScope dbScope,
[Dependency(CardRepositoryNames.Extended)]
ICardRepository cardRepository,
IUnityContainer unityContainer,
IWebHostEnvironment hostEnvironment,
IOptions<WebOptions> options,
ISession session,
ISlugsGenerator slugsGenerator)
{
// ...
}
В объявлении класса отсутствует атрибут [TessaController]
, а значит по умолчанию получение зависимостей будет происходить из Unity
-контейнера. Также видно, что для параметра cardRepository
используется описанный выше атрибут [Dependency]
с параметром CardRepositoryNames.Extended
, следовательно будет получен репозиторий для работы с карточками, учитывающий платформенные и проектные расширения.
Пересчислим дополнительно объекты, которые могут быть полезны при разработке контроллеров:
-
IOptions<T>
- объект-синглтон, содержащий настройки сервиса, тип которых указан в параметреT
. Например:-
WebOptions
- основные настройки из конфигурационного файлаapp.json
веб-сервиса. -
WebServerLimits
- настройки ограничений веб-сервиса из конфигурационного файлаapp.json
(если какие-то настройки не указаны в файле, будут использованы рекомендованные значения по умолчанию). -
WebServerOptions
- настройки веб-сервера, используемого дляKestrel
(подробнее см. руководство) из конфигурационного файлаapp.json
(если какие-то настройки не указаны в файле, будут использованы рекомендованные значения по умолчанию). -
WebClientOptions
- настройки из конфигурационного файлаapp.json
для веб-клиента.
-
-
IWebHostEnvironment
- объект, предоставляющий информацию об окружении, например, имя приложения, путь к корневой папке приложения и прочее. -
ITessaWebScope
- контекст выполнения HTTP-запроса для API TESSA, содержащий токен сессии,Unity
-контейнер с зависимостями, имя экземпляра сервера и признак нахождения веб-сервера в режиме нескольких экземпляров.
Tip
Для получения прочих типовых настроек из конфигурационного файла app.json
веб-сервиса, можно воспользоваться статическим классом ConfigurationManager
.
Особенности получения зависимостей¶
При получении зависимостей через конструктор контроллера, используя DI-контейнер ASP.NET Core для режима активации ControllerActivationMode.Unity
(класс контроллера без атрибута [TessaController]
или с атрибутом, не изменяющим режим активации на другой) следует учитывать следующие особенности:
-
Если параметр конструктора является опциональным (
IService? service = null
), то регистрация зависимости может отсутствовать. В этом случае будет подставлено значение параметраnull
. -
Допустимо использовать атрибут
[FromKeyedServices([Key])]
, чтобы определить ключ, по которому зависимость должна быть зарегистрирована (какAddKeyedScoped
илиAddKeyedSingleton
), но в пределах класса допустимо использовать лишь один ключ (или его отсутствие) для каждого типа параметра, получаемого из DI-контейнер ASP.NET Core. -
Для более сложных сценариев можно получить в конструкторе
IServiceProvider
и запросить из него сервисы вручную, или же отказаться от режима активацииControllerActivationMode.Unity
, указав атрибут[TessaController(ActivationMode = ControllerActivationMode.AspNetCore)]
.
Например, используя один и тот же сервис IService
дважды, нельзя указать разные ключи (возникнет исключение InvalidOperationException
с подробным описанием):
[FromKeyedServices("1")] IService service,
[FromKeyedServices("2")] IService service2
// или
IService service,
[FromKeyedServices("2")] IService service2
Но допускается следующее:
// С простыми параметрами
[FromKeyedServices("1")] IService service,
[FromKeyedServices("1")] IService service2
// и с опциональными (при невозможности получения зависимости
// всегда подставляется null, неважно, что указали в конструкторе)
IService? service = null,
[FromKeyedServices("1")] INewService? newService = null
Методы контроллера¶
Для того, чтобы контроллер обрабатывал запросы, необходимо добавить публичные методы в класс, а также назначить им определенные атрибуты.
Для примера рассмотрим метод PostGetCard
:
/// <summary>
/// Загружает карточку по заданному запросу для клиента.
/// Метод идентичен типовому методу загрузки карточки в контроллере <c>CardsController</c>.
/// Это метод для тестирования возможностей REST веб-сервиса. Метод требует наличия сессии.
/// </summary>
/// <param name="request">Запрос на загрузку карточки.</param>
/// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param>
/// <returns>Ответ на запрос на загрузку карточки.</returns>
// POST service/cards/get
[HttpPost("cards/get"), SessionMethod(UserAccessLevel.Administrator), TypedJsonBody]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CardGetResponse>> PostGetCard(
[FromBody] CardGetRequest request,
CancellationToken cancellationToken = default)
{
CardGetResponse response = // ... отправка запроса и получение ответа
return await this.TypedJsonAsync(response, cancellationToken: cancellationToken);
}
Этот метод загружает карточку по переданному запросу. Разберём его:
-
Описание самого метода в специальном теге
<summary>
, а также описание параметров в тегах<param ...>
- необходимо для отображения этой информации в Swagger. -
Название
PostGetCard
- состоит из префиксаPost
, соответствующего названию HTTP-метода, и названия описательного характера. -
Атрибуты:
-
[HttpPost(...)]
- указывает, что метод должен обрабатывать только POST-запрос. В качестве параметра в нем указан шаблон маршрута. -
[SessionMethod(...)]
- указывает, что метод поддерживает сессию TESSA для проверки доступа. -
[TypedJsonBody(...)]
- указывает, что тело запроса необходимо десериализовать из форматаTypedJSON
. -
[Consumes(...)]
- фильтр, указывающий поддерживаемые типы контента запроса. В данном случае этоapplication/json
. -
[Produces(...)]
- фильтр, указывающий поддерживаемые типы контента ответа. В данном случае этоapplication/json
. -
[ProducesResponseType(...)]
- фильтр, указывающий код состояния, возвращаемый методом. В данном случае это HTTP-код 200, соответствующий успешному запросу.
-
-
Параметры:
-
request
- запрос на получение карточки. Имеет атрибут[FromBody]
, который указывает, что значение должно быть получено из тела запроса. -
cancellationToken
- объект, с помощью которого можно отменить асинхронную операцию.
-
-
Возвращаемое значение типа
ActionResult<...>
- позволяет возвращать ответ определенного типа. -
Метод
TypedJsonAsync
- указывает, что тело ответа будет сериализовано вTypedJSON
с помощью API TESSA.
Информация из атрибутов HttpPost
, Consumes
, Produces
, ProducesResponseType
, FromBody
, описания метода в теге <summary>
и его параметров в <param ...>
, а также выведенная из ActionResult<...>
будет отображена в Swagger, что можно увидеть, если перейти в него, как это было сделано ранее:
Warning
В описании присутствуют HTTP-коды ошибок с указанием модели ответа. Это следует из того, что в описании класса есть специальные атрибуты, которые были разобраны ранее.
Атрибуты и параметры¶
Разберем полезные атрибуты, предоставляемые ASP.NET Core:
Название | Назначение | Применение |
---|---|---|
[Http<Verb>(...)] |
Определяют маршрут и действие, которое обрабатывает данный метод. Вместо <Verb> указывается один из HTTP-методов запроса, а также опционально указывается шаблон маршрута. Если опустить шаблон, то метод будет доступен по базовому пути контроллера |
Необходимо указывать для всех методов, обрабатывающих HTTP-запросы |
[Consumes(...)] |
Фильтр, указывающий поддерживаемые типы контента запроса (подробнее ниже) | Необязательный атрибут, указываемый в объявлении метода. Необходимо применять там, где используется тело запроса |
[Produces(...)] |
Фильтр, указывающий поддерживаемые типы контента ответа (подробнее ниже). Может быть указано несколько типов | Необязательный атрибут, указываемый в объявлении метода. Необходимо применять там, где возвращается тело ответа |
[ProducesResponseType(...)] |
Фильтр, указывающий код состояния, возвращаемый методом. Может быть указано несколько | Необязательный атрибут, указываемый в объявлении метода. Рекомендуется использовать, если метод может возвращать HTTP-коды, отличные от тех, которые были указаны в атрибутах класса |
[From<Part>(...)] |
Указывает источник данных для целевого параметра (подробнее будет рассмотрено далее в этом же разделе) | Необязательный атрибут, указываемый для параметров. Следует использовать, если значение должно быть получено из какой-либо составляющей HTTP-запроса |
[DisableRequestSizeLimit] |
Указывает, что необходимо обрабатывать запросы любого размера. По умолчанию максимальный размер равен 28.6 МБ (30 000 000 байт), но это значение можно изменить в конфигурационном файле app.json веб-сервиса задав настройку Settings -> WebServerLimits -> MaxRequestBodySizeBytes |
Атрибут может быть применен как к конкретному методу, так и ко всему классу. Полезен при передаче в запросе файлов или других данных большого объема |
Существует несколько типов и подтипов контента, используемых для указания в запросах и ответах, но здесь мы рассмотрим только наиболее популярные:
-
application/json
- контент заполнен в формате JSON. -
application/octet-stream
- значение по умолчанию для бинарных данных без дополнительной обработки. Следует использовать для передачи файлов. -
text/html
- текст в формате HTML. -
text/plain
- простой текст. -
text/xml
- текст в формате XML. -
multipart/form-data
- многокомпонентные данные, разделенные специальной границей, где каждая часть имеет отдельный заголовок и тип контента. Может быть использован при отправке значений из заполненной HTML-формы.
Как было описано выше, можно указать целевые источники данных для параметров:
-
[FromQuery(...)]
- значение определено в параметре запроса; -
[FromRoute(...)]
- значение определено в маршруте запроса; -
[FromForm(...)]
- значение определено в поле HTML-формы; -
[FromBody(...)]
- значение определено в теле запроса (может быть применен только для одного параметра); -
[FromHeader(...)]
- значение определено в HTTP-заголовке запроса.
Tip
Если требуется более сложная логика обработки HTTP-заголовков запроса, можно воспользоваться свойством this.Request.Headers
контроллера, которое содержит их коллекцию со значениями.
Атрибут [FromRoute(...)]
сопоставляет параметр со значением с помощью шаблона маршрута. При этом для точного сопоставления типов параметров со значениями, отличными от строк, необходимо явно их указать. Например:
[HttpPost("method/{routeParam:guid}/{named-param}")]
public Task<...> PostMethod(
[FromRoute] Guid routeParam,
[FromRoute(Name = "named-param")] string namedParam) { ... }
Рассмотрим атрибуты, входящие в API TESSA:
Название | Назначение | Применение |
---|---|---|
[TypedJsonBody(...)] |
Указывает, что тело запроса необходимо десериализовать из формата TypedJSON |
Следует применять в том случае, когда в теле запроса приходит TypedJSON , т.е. JSON, в котором есть дополнительная информация о типах объектов, созданная с помощью API TESSA |
[SessionMethod(...)] |
Указывает, что метод поддерживает сессию TESSA для проверки доступа и для передачи информации о клиенте. В качестве его параметра accessLevel указывается уровень доступа |
Следует применять всегда, когда требуется проверять доступ клиента к сервису по токену TESSA |
[SessionToken] |
Указывает, что строковый параметр содержит токен ISessionToken , сериализованный в XML. При этом переданный токен указывается как текущий в контексте запроса |
Работает только совместно с атрибутом [SessionMethod] . Полезен в случаях, когда надо передать токен не в HTTP-заголовках |
[DisableFormValueModelBinding] |
Отключает проброс значений из формы в параметры | Может быть применен как к методу, так и ко всему классу. Полезен в случае, когда все данные из формы требуется обработать самостоятельно |
Для примера объявим POST-метод, который:
-
создаёт или получает некоторый объект;
-
требует наличия токена сессии в заголовке;
-
получает тело в формате
TypedJson
(некоторый тип, наследованный от классаStorageObject
); -
получает идентификатор в маршруте, по которому надо либо найти уже существующий объект, либо создать новый;
-
возвращает соответствующий код при создании нового или нахождении уже имеющегося объекта в системе по указанному идентификатору.
[HttpPost("some-method/{id:guid}"), TypedJsonBody, SessionMethod]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> PostSomeMethod(
[FromRoute] Guid id,
[FromBody] SomeStorageObject bodyParameter,
CancellationToken cancellationToken = default)
{
bool alreadyExists;
// ... создание нового или получение имеющегося объекта.
return alreadyExists
? this.Ok("Already exists")
: this.CreatedAtAction(nameof(this.PostSomeMethod), new { id }, null);
}
Tip
Дополнительные примеры с использованием описанных атрибутов можно найти ниже в разделах Обработка потока запроса и Чтение файлов из мультипарта.
Возвращаемые значения¶
Разберем типы возвращаемых значений:
-
Определенный тип - указывает, что метод возвращает тип, который может быть как примитивным, так и сложным. Таким образом, возвращаемое значение может быть только одного единственного типа. Этот вариант стоит применять, когда метод возвращает простой конкретный результат. Например:
[HttpGet("check")] public async Task<string> GetCheck(CancellationToken cancellationToken = default) { string result; // ... формирование результата. return result; }
-
IActionResult
- данный тип можно использовать, если в результате работы могут быть возвращены объекты различных моделей. Для отображения информации о них в Swagger, необходимо в объявлении метода добавить ранее описанный атрибут[ProducesResponseType(<T>, <StatusCode>)]
, где вместо<T>
подставить тип модели, а вместо<StatusCode>
- HTTP-код ответа. Например:[HttpGet("check")] [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status202Accepted)] public IActionResult GetCheck([FromQuery] int mode) { if (mode < 0) { return this.BadRequest(); } else if (mode == 0) { return this.Accepted("Everything is fine"); }
return this.Ok(mode); }
-
ActionResult<T>
- преобразуемый вIActionResult
тип с некоторыми преимуществами:-
Атрибут
[ProducesResponseType(...)]
можно указывать без параметраT
, т.к. тип будет выведен автоматически. -
Можно использовать неявное приведение из типов
ActionResult
иT
вActionResult<T>
, т.е. можно возвращать объект любого из перечисленных типов.
Например:
[HttpGet("check")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<string>> GetCheck( CancellationToken cancellationToken = default) { bool isError = // ... определение наличия какой-либо ошибки. if (isError) { return this.BadRequest(); }
return "Everything is fine"; }
-
Tip
Методы контроллера могут возвращать асинхронный результат, для этого возвращаемое значение необходимо обернуть в класс Task<T>
(например, Task<IActionResult>
). Также допустимо использовать токен отмены асинхронной операции CancellationToken
. Его срабатывание происходит в момент отмены токена из контекста запроса HttpContext.RequestAborted
, т.е. в случае, когда соединение разорвано по инициативе клиента или сервера (например, сработал клиентский или серверный таймаут, или пользователь закрыл окно браузера).
Important
Не передавайте CancellationToken
в методы, которые обрабатывают ошибки или очищают ресурсы:
IOperationRepository operationRepository = // ...
IErrorManager errorManager = // ...
List<IOperation>? ids = null;
try
{
// Указывается токен отмены, т.к. операцию можно отменять.
ids = await operationRepository.GetAllAsync(
cancellationToken: cancellationToken);
}
finally
{
if (ids is { Length: 0 })
{
// Здесь не указывается токен отмены, т.к. карточка ошибки
// должна создаться в любом случае.
await errorManager.ReportErrorSafeAsync(
// Список параметров, в котором
// отсутствует CancellationToken.
);
}
}
Для возврата ответа с HTTP-кодами, указывающими на то, что операция прошла успешно, в классе Controller
и API TESSA существует несколько удобных методов. Рассмотрим наиболее популярные из них:
Название | HTTP-код | Описание |
---|---|---|
Ok |
200 | Запрос был корректно обработан |
Json |
200 | Запрос был корректно обработан, при этом тело ответа будет сериализовано в JSON. Рекомендуется использовать, когда необходимо возвращать тело в формате JSON для простого объекта, не требующего предоставление информации о типах его полей |
TypedJson |
200 или 204 | Запрос был корректно обработан, при этом тело ответа будет сериализовано в TypedJSON (для HTTP-кода 200), либо возвращено пустым (для HTTP-кода 204). Рекомендуется использовать, когда необходимо возвращать тело в формате JSON для объекта, требующего предоставление информации о типах его полей |
Created , CreatedAtAction , CreatedAtRoute |
201 | Запрос был успешно выполнен, а также в результате был создан новый ресурс. Обычно ассоциируются с POST-методами. Рекомендуется использовать в случае, если обработка запроса подразумевает создание некоторого объекта, при этом в теле ответа необходимо указать информацию о созданном объекте, например, идентификатор |
NoContent |
204 | Запрос был выполнено успешно, но тело ответа не заполнено. Следует использовать, когда в ответе не содержится никакой полезной нагрузки |
Также в контроллерах есть методы, возвращающие ответы с HTTP-кодами ошибок:
Название | HTTP-код | Описание |
---|---|---|
BadRequest |
400 | Запрос, отправленный на сервер, некорректен или поврежден. Следует использовать, когда, например, нет обязательных параметров в запросе, или наоборот, есть лишние данные, нет заголовков и т.п. |
Unauthorized |
401 | В запросе отсутствуют действительные учетные данные для проверки доступа к целевому ресурсу. Следует использовать, когда, например, в заголовках запроса должен был находиться токен для авторизации, но он не был найден |
Forbid |
403 | У клиента, отправившего запрос, нет доступа к данным. В отличии от Unauthorized , который проверяет, что учётные данные для проверки просто должны быть заданы, этот метод указывает, что отсутствует доступ именно из-за ограничений, определённых на основании переданных учётных данных |
NotFound |
404 | Запрошенный ресурс не существует. Например, если есть GET-метод, параметры которого задаются через URL (GET /service/{id:guid} ), но не найден объект, id которого указан в запросе |
Note
Unauthorized
возвращается по умолчанию для контроллеров и их методов, помеченных атрибутом [SessionMethod]
, в случае, если в заголовках запроса не найден токен по ключу Tessa-Session
.
Tip
Для того, чтобы в методе контроллера предоставить исчерпывающую информацию при возникновении ошибки, соответствующей Forbidden
, можно воспользоваться методом-расширением ControllerExtensions.Forbidden
:
using Tessa.Web.Helpers;
// ...
try
{
bool validationResultError;
// ... проверка доступа к некоторому ресурсу.
if (validationResultError)
{
// Возвращает объект типа PlainValidationResult.
throw this.ForbiddenException("ValidationResultErrorText");
}
}
catch (ValidationException ex)
{
throw;
}
catch (Exception ex)
{
// Возвращает ObjectResult.
return this.Forbidden(ex.Message);
}
Необходимо также упомянуть специальные методы для редиректа, как ответа на запрос:
-
View
- стандартный метод из классаController
. Предоставляет страницу, на которую произойдет редирект в результате запроса. По умолчанию HTTP-код ответа - 200, но его можно изменить через свойствоStatusCode
. -
ErrorView
- метод из API TESSA, который расширяет функционал методаView
. Предоставляет редирект на стандартную для TESSA страницу с ошибкой.
К примеру, осуществить редирект на стандартную страницу ошибки TESSA с указанием её HTTP-кода можно следующим образом:
try
{
// ... код, вызывающий ошибку.
}
catch(Exception ex) when (ex is not OperationCanceledException)
{
return this.ErrorView(ex, statusCode: HttpStatusCode.InternalServerError);
}
Note
Метод this.ErrorView
может не принимать HTTP-код ошибки в качестве параметра. В этом случае будет вызван метод-расширение ex.GetStatusCode()
, который определяет HTTP-код в зависимости от исключения (подробнее см. в следующем разделе), и использует код 400 для прочих неизвестных исключений.
Note
В случае, если необходимо установить HTTP-заголовки в ответе, необходимо задать значения для свойства this.Response.Headers
контроллера.
Обработка исключений¶
В процессе обработки запроса могут возникать исключения. В TESSA все исключения в методах контроллеров, кроме тех, которые возникают уже после начала отправки (например, когда происходит отправка потока) сериализуются в JSON типа PlainValidationResult
. Поэтому для контроллеров рекомендуется устанавливать атрибут [ProducesErrorResponseType(typeof(PlainValidationResult))]
, чтобы в Swagger можно было увидеть информацию об этом.
Warning
По умолчанию в результат попадает информация о трассировке стека исключения, но с помощью флага SecureServerStackTrace
его можно скрыть (подробнее см. в разделе Настройка production сервера).
При возникновении исключений в ответе указывается HTTP-код:
-
401 (Unauthorized) - для исключений типа
UnauthorizedAccessException
; -
408 (RequestTimeout) - для исключений типа
TimeoutException
; -
Код ошибки, указанный в свойстве
StatusCode
для исключений, реализующих интерфейсIHttpStatusCodeProvider
API TESSA; -
400 (BadRequest) - для всех остальных исключений.
В API TESSA для записи информации об ошибке часто используется объект IValidationResultBuilder
и ValidationResult
, с помощью которых можно создать исключение ValidationException
и установить код ответа:
CardGetResponse response = // ... запрос в репозиторий карточек и запись ответа.
if (!response.ValidationResult.IsSuccessful())
{
var validationResult = response.ValidationResult.Build();
throw new ValidationException(validationResult)
{
StatusCode = HttpStatusCode.InternalServerError
};
}
Здесь можно увидеть, что в случае ошибки в ответе на получение карточки из репозитория создаётся исключение ValidationException
, а также, в качестве примера, устанавливается HTTP-код ответа InternalServerError
(500). Если вызвать этот метод и указать, например, идентификатор несуществующей карточки, то в ответе можно будет увидеть код этой ошибки и информацию, полученную из ValidationResult
.
Обработка потока запроса¶
В случае работы с большим объемом данных (например, при работе с файлами) можно обработать тело запроса как поток.
Получение потока через параметр запроса¶
В данном случае поток будет получен как параметр метода, для которого применен атрибут [FromBody]
:
[HttpPost("method"), DisableRequestSizeLimit]
[Consumes(MediaTypeNames.Application.Octet)]
// ... прочие атрибуты
public async Task<IActionResult> CreateFile(
[FromBody] Stream contentStream,
CancellationToken cancellationToken = default) { ... }
Но если тело запроса окажется пустым (например, был передан пустой текстовый файл), вызов метода закончится неудачей.
Получение потока через объект запроса¶
Здесь поток будет получен напрямую из объекта запроса, который является свойством класса контроллера (при этом пустое тело обрабатывается без ошибок):
[HttpPost("method"), DisableRequestSizeLimit]
[Consumes(MediaTypeNames.Application.Octet)]
// ... прочие атрибуты
public async Task<IActionResult> PostMethod(
CancellationToken cancellationToken = default)
{
Stream contentStream = this.Request.Body;
// ... обработка потока.
}
Important
Обратите внимание, что прочитать тело запроса из потока возможно только один раз, при этом запрос не кешируется. Подробнее см. в MSDN.
Important
При работе с телом запроса в виде потока необходимо учитывать, что, в случае возникновения исключения, поток необходимо дочитать до конца, вызвав у него метод DrainAsync
. Иначе может возникнуть ошибка из-за непрочитанного запроса.
Чтение файлов из мультипарта¶
Рассмотрим варианты чтения запроса, у которого тип контента указан как multipart/form-data
:
-
Чтение с использованием атрибута
[FromForm]
:[HttpPost("method"), DisableRequestSizeLimit] [Consumes(MediaTypeNames.MultipartFormData)] // ... прочие атрибуты public async Task<IActionResult> PostMethod( [FromForm] IFormFile fileForm, [FromForm] Guid id, CancellationToken cancellationToken = default) { ... }
где параметры
fileForm
иid
будут автоматически прочитаны из формы. Также можно создать собственный класс и использовать его в качестве типа параметра. -
Чтение мультипарта напрямую из объекта запроса, который является свойством контроллера
this.Request.Body
. Подробную информацию можно найти в примере REST-методы для работы с multipart.
Отправка файлов¶
Популярной задачей, которую решают контроллеры, является отправка файлов в ответ на запрос. Рассмотрим несколько примеров того, как это можно реализовать.
Отправка файла вложением¶
Первый вариант, это отправка файлов вложением, т.е. таким образом, что браузер должен скачает полученный контент:
[HttpPost("get-file-content"), SessionMethod, TypedJsonBody]
[Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Octet, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> PostGetFileContent(
[FromBody] CardGetFileContentRequest request,
CancellationToken cancellationToken = default)
{
// ... подготовка запроса
return await FileContentHelper.GetFileContentAsync(
this.cardStreamServerRepository,
this.Response,
request,
cancellationToken);
}
На вход метод имеет запрос на получение файла из репозитория, а в ответ отдаёт объект, доступный для скачивания.
Метод FileContentHelper.GetFileContent
уже содержит логику для получения файлов и заполнения ответа, поэтому им рекомендуется пользоваться, если возникла похожая задача. Если логика получения файла отличается, можно реализовать заполнение ответа, как в следующем примере:
string fileName = // ... получение имени файла для скачивания.
long fileSize = // ... получение размера контента.
string contentType = // ... определение типа контента.
Stream? stream = // ... получение потока с контентом.
try
{
// В заголовок ответа проставляется признак того, что файл является вложением
// и должен быть скачен браузером.
this.Response.Headers.ContentDisposition = FileContentHelper.GetAttachmentContentDisposition(fileName);
this.Response.ContentLength = fileSize;
// Создание потока с контентом, как результат запроса.
var streamResult = new FileStreamResult(stream, mime);
// Поток не надо закрывать в случае успеха.
stream = null;
return streamResult;
}
catch
{
// ... закрытие поток в случае ошибки.
}
Отправка файла как multipart¶
Также файл можно отправить с типом контента multipart/form-data
. Подробную информацию можно найти в примере REST-методы для работы с multipart.
Открытие и закрытие сессии¶
На примере методов контроллера ServiceController
рассмотрим открытие и закрытие пользовательской сессии.
Открытие сессии¶
В контроллере представлено два метода для открытия сессии. Сначала разберём метод PostLogin
:
// POST service/login
[HttpPost("login"), TypedJsonBody(ConvertPascalCasing = true)]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Text.Xml, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<string>> PostLogin(
[FromBody] IntegrationLoginParameters parameters,
CancellationToken cancellationToken = default)
{
OpenSessionResult result = // ... открытие сессии с указанием логика и пароля.
// Токен должен быть сериализован в XML.
// В качестве параметра передаётся объект с настройками,
// определяющими версию TESSA и объём заполняемых данных.
var authToken = result.Token.SerializeToXml(SessionSerializationOptions.Auth);
// Сервер сообщает версию и токен только после того, как сессия была успешно открыта,
// т.е. это пользователь системы, а не случайный человек.
this.Response.Headers[SessionHttpRequestHeader.Version] = BuildInfo.VersionObjectString;
this.Response.Headers[SessionHttpRequestHeader.Session] = authToken;
// Возвращает токен в формате XML и HTTP-код ответа 200.
return this.Ok(authToken);
}
Данный метод осуществляет открытие пользовательской сессии на основании объекта, содержащего логин и пароль, и возвращает версию TESSA и токен авторизации в заголовках и в теле ответа в формате XML. Также необходимо отметить следующие моменты:
-
В объявлении метода указан атрибут
[TypedJsonBody(ConvertPascalCasing = true)]
для того, чтобы можно было принять как простойJSON
, так иTypedJSON
. При этом флагConvertPascalCasing
указывает, что необходимо преобразовать первую строчную букву в прописную в названиях свойств объектов. Например:userName
=>UserName
. -
Возвращаемая строка с токеном должна быть проброшена в HTTP-заголовок
Tessa-Session
для дальнейшего взаимодействия с системой.
Теперь рассмотрим метод PostWinLogin
:
// POST service/winlogin
[HttpPost("winlogin"), Produces(MediaTypeNames.Text.Xml, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<string>> PostWinLogin(CancellationToken cancellationToken = default)
{
IIdentity? identity = this.HttpContext.User.Identity;
if (identity is null)
{
// ... выброс исключения.
}
string? accountName;
// Если пользователь не аутентифицирован или имя аккаунта не заполнено, необходимо вернуть ошибку.
if (!identity.IsAuthenticated || string.IsNullOrEmpty(accountName = identity.Name))
{
// ... выброс исключения.
}
OpenSessionResult result = // ... открытие сессии с указанием имени аккаунта.
// ... аналогичные методу PostLogin действия для возврата токена.
}
Данный метод осуществляет открытие сессии, используя Windows-аутентификацию, и возвращает версию TESSA и токен авторизации в заголовках и теле ответа в формате XML.
Закрытие сессии¶
Рассмотрим теперь метод закрытия сессии PostLogout
:
[HttpPost("logout"), SessionMethod]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> PostLogout(
[FromQuery, SessionToken] string? token = null,
CancellationToken cancellationToken = default)
{
await this.sessionService.CloseSessionWithTokenAsync(token, cancellationToken);
return this.NoContent();
}
Данный метод закрывает сессию, токен которой передан в HTTP-заголовке Tessa-Session
или в адресной строке в параметре token
. Метод не возвращает тело, но указывает HTTP-код ответа 204.
Important
Методы PostLogin
, PostWinLogin
и PostLogout
сделаны исключительно для примера. Для взаимодействия с сессией из REST API, лучше использовать методы по маршруту https://<адрес веб-сервиса TESSA>/api/v2/sessions
.
Выполнение метода без токена сессии¶
В некоторых ситуациях может потребоваться вызывать некоторый метод контроллера без атрибута [SessionMethod]
. Это означает, что в контексте запроса IWebContext
будет отсутствовать информация о токене сессии, и, как следствие, при обращении к API TESSA будут возникать ошибки. В таком случае можно создать контекст сессии в самом методе с помощью объекта SessionContext
:
// (1) Создание токена для платформенного пользователя `System`.
ISessionToken token = Session.CreateSystemToken(this.serverSettings);
// (2) Или создание токена с указанием конкретного пользователя и прочих настроек.
ISessionToken token = new SessionToken(
// ... инициализация параметров
);
// Создание контекста.
await using (SessionContext.Create(token))
{
// ... взаимодействие с API TESSA.
}
Внутри using
код будет выполняться от имени пользователя System
в первом случае или от имени заданного пользователя во втором случае.
Important
Необходимо помнить, что при написании методов, не использующих атрибут SessionMethod
, ответственность за несанкционированный доступ к данным несёт сам разработчик. Поэтому написание таких методов требует весомых причин.
Рекомендуется также написать клиентские тесты для проверки отсутствия доступа к методу для тех пользователей, у которых его не должно быть.
Обработка запроса с JWT-токеном¶
Во время разработки контроллера может возникнуть задача реализации собственной авторизации. Одним из вариантов её решения является реализация авторизации на основе JWT-токена.
JSON Web Token - это открытый стандарт, который определяет компактный и автономный способ безопасной передачи информации между сторонами в виде JSON. Более подробно можно прочитать в документации.
Для примера разберём создание метода, который получает JWT-токен для авторизации с помощью Bearer. В этом случае в HTTP-запросе присутствует следующий заголовок:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.SdUMMAsFFk83SHyqGjfQ1pt8v_8t4WZku5Ty3CLMaH4
где после ключевого слова Bearer
записан сформированный токен.
Подразумевается, что клиент делает запрос на сервер для получения токена, при этом он указывает свои учётные данные. Сервер принимает запрос, обрабатывает его и отдаёт токен, который запоминается клиентом. Опустим написание метода контроллера для возврата токена, но рассмотрим код для его формирования:
string issuer; // Центр авторизации.
byte[] publicKey, privateKey; // Публичный и приватный ключи.
TimeSpan expiration; // Время жизни.
// Импорт ключей и формирование удостоверения для подписи.
var dsa = ECDsa.Create();
dsa.ImportSubjectPublicKeyInfo(publicKey, out _);
dsa.ImportPkcs8PrivateKey(privateKey, out _);
var securityKey = new ECDsaSecurityKey(dsa)
{
KeyId = Convert.ToBase64String(NotNullOrThrow(key.KeyID))
};
var signingCredentials = new SigningCredentials(securityKey, "ES512");
// Формирование токена.
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
issuer: issuer,
subject: new ClaimsIdentity(
new Claim[]
{
new("scope", "testScope", ClaimValueTypes.String),
}),
expires: DateTime.UtcNow.Add(expiration),
signingCredentials: signingCredentials);
string result = handler.WriteToken(token);
Реализуем метод контроллера, который считает и проверит его:
[HttpGet("check-bearer-token"), Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
// ... прочие заголовки
public async Task<IActionResult> GetCheckBearerToken(CancellationToken cancellationToken = default)
{
string jwtToken = // ... получение токена из заголовков.
// Проверка валидности токена.
var securedToken = this.CheckBearerToken(jwtToken);
if (securedToken is null)
{
return this.BadRequest();
}
// Проверяем, что токен содержит правило `testScope`.
if (!securedToken.Claims.Any(c => c.ValueType == ClaimValueTypes.String && c.Value == "testScope"))
{
return this.BadRequest();
}
// ... если прошли проверку, то, например, откроем сессию под пользователем `System`
// и выполним какую-то полезную операцию.
}
Проверить валидность ключа можно следующим образом:
string token;
try
{
TokenValidationParameters validationParameters = // ... указание параметров валидации.
// Проверка валидности ключа.
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
}
catch (SecurityTokenException ex)
{
// Если токен не валиден, возникнет исключение.
}
Создание клиентского API для обращения к контроллеру¶
Теперь рассмотрим средства для создания клиентского API, которое может обращаться к методам контроллера.
Note
В этом разделе рассматривается создание API только для веб-клиента. Пример разработки аналогичного функционала на языке C# можно найти в соответствующем разделе.
Warning
Важно заметить, что компоненты системы могут общаться между собой по HTTP, как, например, взаимодействуют chronos и web-сервис с jinni, но не стоит организовывать такое же взаимодействие между chronos и web-сервисом, т.к. они оба имеют доступ к СУБД и файловому хранилищу, на которых развёрнут экземпляр TESSA.
Для написания собственного клиента, обращающегося к контроллеру, в веб-клиенте будут выполняться импорты из модулей (есть по умолчанию в WebClient SDK
):
-
@tessa/core
- предоставляет вспомогательные методы и объекты для работы со storage-объектами, валидацией, сериализацией, форматерами и прочим. -
@tessa/application
- предоставляет объекты, позволяющие осуществлять HTTP-запросы, работать с локализацией и токенами сессии, а также использовать DI-контейнер. -
@tessa/platform
- предоставляет базовые объекты для работы с API TESSA.
Пример из типового решения¶
Рассмотрим клиентский сервис из типового решения, который зарегистрирован в DI-контейнере и обращается к ServiceController
(файл 22_serviceClient.ts
из типового решения):
@injectable()
export class ServiceClient { ... }
Класс ServiceClient
помечен атрибутом @injectable()
, что даёт возможность использовать его для внедрения. Регистрируется он следующим образом:
export const ServiceClient$ = createInjectToken<ServiceClient>('ServiceClient');
Тогда получить его экземпляр из DI-контейнера в конструкторе можно так:
constructor(@inject(ServiceClient$) serviceClient: ServiceClient) { ... }
В самом сервисе для взаимодействия с контроллерами TESSA используется объект IApiClient
, который также внедряется из DI-контейнера. Этот сервис рекомендуется использовать, т.к. он предоставляет достаточный базовый функционал отправки запросов и обработки ответов с учётом пользовательских сессий.
Рассмотрим метод getCard
, который отправляет запрос на сервер для получения карточки:
async getCard(request: CardGetRequest): Promise<CardGetResponse> {
const storage = await this._apiClient
.post('/service/cards/get', {
headers: {
[HttpRequestHeaders.Session]: assertNotNull(this._token)
},
typedJson: request.getStorage()
})
.typedJson();
return new CardGetResponse(storage);
}
Внутри реализована следующая логика:
-
Отправка POST-запроса по маршруту
https://<адрес веб-сервера>/service/cards/get
(сервисIHttpClient
, который является базовым дляIApiClient
, сам добавляет адрес веб-сервера, поэтому необходимо указывать только часть с маршрутом контроллера). -
В заголовки добавляется токен
Tessa-Session
(обычно он добавляется автоматически, но для примера в конструкторе эта опция была отключена). -
Тело запроса передаётся в формате
TypedJSON
. -
Тело ответа обрабатывается как
TypedJSON
, а затем сохраняется как storage-объект.
Tip
В сервисе IHttpClient
есть все необходимые методы для отправки различных видов HTTP-запросов, которые также принимают набор параметров HttpOptions
, влияющих на выполнение запроса и обработку результата.
Note
Тип контента запроса определяется автоматически, но при необходимости его можно изменить, добавив соответствующие заголовки, по аналогии, как это было сделано выше.
Если необходимо использовать параметры в самом маршруте, то их следует указать, как это сделано в методе getCardById
:
await this._apiClient.get(`/service/cards/${encodeURIComponent(cardId)}`)
Important
Обратите внимание на кодирование параметра cardId
методом encodeURIComponent
. Это нужно для того, чтобы результирующая строка была в виде, удовлетворяющем требованиям HTTP.
Также в этом методе можно увидеть, как передавать query-параметры:
searchParams: {
type: cardTypeName
}
В случае необходимости обработки заголовков ответа, можно получить объект ответа и использовать его свойства. Например:
const response = apiClient
.post(...)
.response();
const responseStorage = await response.typedJson();
const someHeader = response.headers.get('SomeHeaderName');
Если в результате запроса вернулся HTTP-код ответа, соответствующий ошибке, а в опциях запроса установлен флаг throwHttpErrors
, будет выдано исключение.
Tip
В разделе REST-методы для работы с multipart можно найти пример с отправкой файла из web-клиента в виде multipart/form-data
.
Отправка запроса без обновления времени последней активности сессии¶
Как известно, при каждом запросе с указанием токена сессии обновляется время её последней активности. Это корректное поведение, но оно не всегда требуется.
В качестве примера можно взять фоновую задачу обновления узла дерева (когда было добавлено соответствующее расширение): если пользователь оставит окно браузера открытым, периодически будет происходить запрос на сервер для получения актуальных данных.
Чтобы снизить нагрузку, в фоновой задаче при запросе отключается обновление времени последней активности.
Для отключения обновления даты последней активности сессии можно создать свою область выполнения запроса:
const scope = HttpHooksContext.create(
new HttpHooksContext([
{
beforeRequest: async ctx =>
ctx.request.headers.append(HttpRequestHeaders.RequestType, 'background')
}
])
);
и вызывать в её контексте все запросы:
scope.run(() => { ... });
Отключение обновления времени последней активности происходит за счёт указания HTTP-заголовка Tessa-Request-Type
со значением background
.
Tip
HttpHooksContext
возможно использовать не только в рамках указанной задачи. В качестве параметра конструктора можно передать массив хуков, которые будут отрабатывать в рамках текущего контекста для всех запросов.