Перейти к основному содержимому

Разработка контроллера веб-API сервиса бизнес-логики

Разработка контроллера веб-API для сервиса бизнес-логики имеет свои особенности, отличающие его от стандартных контроллеров. В этой статье рассматриваются ключевые аспекты: от выбора HTTP-методов и внедрения зависимостей до оформления ответов и обработки ошибок в едином формате, принятом в Атомкод.

Выбор HTTP-методов

Для вызова методов API сервиса бизнес-логики рекомендуется использовать HTTP-метод POST. В отличие от GET, который предназначен для получения данных без изменения состояния системы, POST является более универсальным. POST позволяет передавать сложные параметры в формате JSON в теле запроса, не имеет ограничений на длину URL и семантически лучше подходит для операций, которые инициируют выполнение бизнес-процессов на сервере. Эти преимущества упрощают взаимодействие с другими сервисами и пользовательским интерфейсом.

Следующий блок кода демонстрирует базовое объявление контроллера API с методом, использующим HTTP POST.

[Route("/api/[controller]")]
[ApiController]
public class MyFirstController : ControllerBase
{
...

[Route("Greeting")]
[HttpPost]
public HelloApiResult Greeting(string name) {
...

Внедрение зависимостей

Одним из ключевых преимуществ ASP.NET Core является мощная встроенная система внедрения зависимостей (Dependency Injection). Через конструктор контроллера можно легко получить доступ ко всем зарегистрированным в приложении сервисам, например, к средствам ведения журнала событий, конфигурации, репозиториям, клиентам других API. Такой подход облегчает читаемость, упрощает тестирование и повышет модульность кода.

Следующий блок кода демонстрирует, как получить экземпляры сервисов ILogger и IConfiguration через механизм внедрения зависимостей в конструкторе контроллера.

[Route("/api/[controller]")]
[ApiController]
public class MyFirstController : ControllerBase
{
private readonly ILogger _logger;
private readonly IConfiguration _config;

public MyFirstController(ILogger<MyFirstController> logger, IConfiguration config)
{
_logger = logger;
_config = config;
...

Генерация документации OpenAPI

Для удобства потребителей вашего API важно иметь актуальную и подробную документацию. В ASP.NET Core с этой целью используется Swashbuckle.AspNetCore, который генерирует спецификацию OpenAPI. Атрибуты [SwaggerOperation] и [SwaggerResponse] позволяют детально описать каждый метод, его возможные ответы и коды ошибок, что делает документацию не только автоматически генерируемой, но и информативной.

Следующий блок кода демонстрирует добавление методу Create атрибутов для настройки его отображения в Swagger-документации. Атрибут [SwaggerOperation] задает уникальный идентификатор операции и теги для группировки, а [SwaggerResponse] явно указывает, какой объект возвращается при успешном выполнении (код 200).

[HttpPost]
[Route("create")]
[Produces("application/json")]
[SwaggerOperation(OperationId = "Create", Tags = [Constants.EventTypesTag])]
[SwaggerResponse(200, "Результат выполнения операции", Type = typeof(EventTypeApiResult))]
public async Task<EventTypeApiResult> Create([FromBody] EventTypeParameter data)
{
...

Формат возвращаемого результата

Для обеспечения единообразных и структурированных ответов все методы API должны возвращать результат в стандартизированном формате. Вместо того чтобы возвращать сырые данные или использовать ActionResult, рекомендуется использовать класс-обертку, который содержит как данные, так и метаинформацию о выполнении запроса, например, успех/ошибка, код ошибки, сообщения.

Следующий блок кода демонстрирует пример объявления класса EventTypeApiResult, который является наследником универсального базового класса ApiResult<TResult>. Такой подход гарантирует единый формат всех ответов API.

public class EventTypeApiResult : ApiResult<EventTypeResult>
{
/// <summary>
/// Конструктор без параметров
/// </summary>
public EventTypeApiResult()
: base()
{ }

/// <summary>
/// Конструктор с аргументом-возвращаемым значением
/// </summary>
/// <param name="result">Результат вызова</param>
public EventTypeApiResult(EventTypeResult result)
: base(result)
{ }

/// <summary>
/// Конструктор с аргументом-исключением
/// </summary>
/// <param name="e">информация об ошибке</param>
public EventTypeApiResult(Exception e)
: base(e, errorCode)
{ }

/// <summary>
/// Конструктор с аргументами-исключением и кодом ошибки
/// </summary>
/// <param name="e">информация об ошибке</param>
/// <param name="errorCode">код ошибки</param>
public EventTypeApiResult(Exception e, string errorCode)
: base(e, errorCode)
{ }
}

Следующий блок кода демонстрирует, как использовать наследника ApiResult для возврата успешного результата выполнения бизнес-операции. Данные упаковываются в экземпляр EventTypeApiResult, что обеспечивает их корректную сериализацию в стандартный формат ответа.

public async Task<EventTypeApiResult> Create([FromBody] EventTypeParameter data)
{
// Логика метода: валидация, вызов сервисов, работа с БД
...
var result = ... // Результат работы метода
...

// Возврат успешного результата
return new EventTypeApiResult(result);
}

Централизованная обработка исключений

Любой метод API должен быть готов обработать исключительную ситуацию и вернуть клиенту понятную ошибку в том же стандартном формате, что и успешный ответ. Такой подход позволяет клиентскому приложению единообразно анализировать и извлекать данные из любых ответов от сервера.

Следующий блок кода демонстрирует обработку исключений внутри метода API с помощью try-catch. Разные типы исключений преобразуются в EventTypeApiResult с соответствующим кодом и сообщением об ошибке, а также регистрируются.

public async Task<EventTypeApiResult> Create([FromBody] EventTypeParameter data)
{
try
{
... // Логика метода
var result = ...
...
return new EventTypeApiResult(result); // Успех
}
catch (BaseException ex) // Обработка ожидаемых бизнес-ошибок
{
_logger.LogError(ex, "Ошибка при создании EventType.");
return new EventTypeApiResult(ex); // Возврат ошибки с кодом из исключения
}
catch (Exception ex) // Обработка непредвиденных ошибок
{
_logger.LogError(ex, "Неизвестная ошибка.");
return new EventTypeApiResult(ex, "CustomErrorCode"); // Возврат ошибки с общим кодом
}
}

Создание типизированных исключений

Чтобы эффективно разделять типы ошибок, например, связанных с некорректными данными от пользователя, нарушением бизнес-правил, правами доступа и безопасностью, рекомендуется создавать собственные классы исключений, наследуемые от BaseException. Такой подход позволяет централизованно управлять кодами ошибок и их сообщениями.

Следующий блок кода демонстрирует пример объявления пользовательского класса-исключения EventTypeException, который содержит конструкторы для удобного создания ошибок с определенным сообщением и кодом.

[Serializable]
public class EventTypeException : BaseException
{
/// <summary>
/// Конструктор без параметров
/// </summary>
public EventTypeException()
{ }

/// <summary>
/// Конструктор с аргументом-сообщением об ошибке
/// </summary>
/// <param name="message">Сообщение об ошибке</param>
public EventTypeException(string message)
: base(message, "MyErrorCode")
{ }

/// <summary>
/// Конструктор с аргументами-сообщением и кодом ошибки
/// </summary>
/// <param name="message">Сообщение об ошибке</param>
/// <param name="errorCode">Код ошибки</param>
public EventTypeException(string message, string errorCode)
: base(message, errorCode)
{ }
}