Бекенд на C# Web Api

To Kaiten

Controllers, Actions, Services

В C# WebAPI контроллеры — это классы, которые обрабатывают HTTP-запросы и возвращают ответы. Они предоставляют интерфейс для взаимодействия с вашим приложением через REST API. Контроллеры наследуются от класса ControllerBase и используются для обработки различных типов запросов, таких как GET, POST, PUT, DELETE и другие.

В контексте ASP.NET Core Web API, Actions (действия) — это методы, которые обрабатывают HTTP-запросы и возвращают HTTP-ответы. Эти методы определяются в контроллерах и отвечают за логику взаимодействия с клиентами API, такие как получение данных, создание новых записей, обновление или удаление ресурсов.

Имя класса контроллера должно заканчиваться словом Controller. Сам класс должен иметь атрибуты ApiController и Route. Пример контроллера представлен ниже.

using Microsoft.AspNetCore.Mvc;

namespace DemoWebApi.Controllers
{
    [ApiController]
    [Route("api/account")]
    public class AccountController : ControllerBase
    {
        [HttpPost("registration")]
        public async Task<IActionResult> Login()
        { ... }

        [HttpGet("login/refresh")]
        public async Task<IActionResult> Refresh()
        { ... }
     }
}

В этом контроллере объявлено два метода (Action'а). В атрибуте Route указан маршрут до контроллера. В результате, данный контроллер описывает следующий API:

POST https://example-service.ru/api/account/registration

GET https://example-service.ru/api/account/login/refresh

Передача параметров в Action

Существует несколько способов передачи параметров. Ниже приведено несколько вариантов того, как action'ы описывают получение параметров.

using Entities.Models;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebApi.Controllers
{
    [ApiController]
    [Route("api/profile/{playerId}")]
    public class ProfileController : ControllerBase
    {
        // /api/profile/player123
        [HttpGet]
        public async Task<PlayerProfile> GetPlayerProfile(string playerId)
        { ... }

        // /api/profile/player123?count=10&type=all
        [HttpGet]
        public async Task<PlayerProfile> GetAchivements(string playerId, [FromQuery] string type, [FromQuery] int count)
        { ... }

        // /api/profile/player123/upload-avatar/avatar123
        [HttpPut("upload-avatar/{avatarId}")]
        public async Task<IActionResult> UploadAvatar(string playerId, string avatarId)
        { ... }

        // /api/profile/player123/update
        [HttpPost("update")]
        public async Task<IActionResult> Update(string playerId, [FromBody] PlayerProfileDto profile)
        { ... }
    }
}
  • Из Route контроллера (все методы) — в фигурных скобках указано название параметра. Этот параметр передается во все action'ы контроллера.

  • Query параметры (метод GetAchivements) — с помощью атрибута FromQuery обозначаются параметры, которые передаются в URL строке после знака "?".

  • Из Route Action'а (метод UploadAvatar) — в фигурных скобках указывается название параметра, которое передается только в текущий action.

  • Из тела запроса (метод Update) — с помощью атрибута FromBody обозначаются параметры, которые передаются в теле запроса. Можно создавать кастомные структуры и указывать их для передачи. Такие структуры должны передаваться в формате json.

Возвращаемые значения

В большинстве случаев достаточно будет возвращать тип IActionResult. Это интерфейс, позволяющий возвращать один из следующих предопределенных типов:

  • OK() — возвращает успешный HTTP-ответ с кодом 200 (OK).

  • BadRequest() — возвращает код 400 (Bad Request), если запрос некорректен.

  • NotFound() — возвращает код 404 (Not Found), если ресурс не найден.

  • NoContent() — возвращает код 204 (No Content) без содержимого.

  • Created(), CreatedAtRoute(), CreatedAtAction() — возвращает код 201 (Created) с URI созданного ресурса.

  • Unauthorized() — возвращает код 401 (Unauthorized), если у пользователя нет доступа.

  • Forbid() — возвращает код 403 (Forbidden), если запрос запрещен.

  • StatusCode() — возвращает код, который передается в параметры метода.

Ниже приведен пример action'а, который проверяет входящие параметры и возвращает Ok или BadRequest.

[HttpPost("update")]
public async Task<IActionResult> Update(string playerId, [FromBody] PlayerProfileDto profile)
{
    if (profile == null)
        return BadRequest("Profile object is null");

    if (ModelState.IsValid == false)
        return BadRequest("Invalid profile object");
    
    _service.Register(profile);

    return Ok();
}

Если нужно вернуть какой-то объект, то можно передать его в метод Ok(), либо воспользоваться возвращаемым типом ActionResult<T> или кастомным типом. При использовании кастомного типа не получится воспользоваться методами Ok, BadRequest и др. Ниже приведен пример трех одинаковых методов, которые по разному возвращают значения: с помощью IActionResult, с помощью ActionResult<T> и с помощью кастомного типа.

// Использование IActionResult
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginCredentialsDto loginData)
{
    var loginTokens = await _service.Login(loginData);

    if (loginTokens == null)
        return BadRequest("Invalid credentials");

    return Ok(loginTokens);
}

// Использование ActionResult<T> (аналогично IActionResult)
[HttpPost("login")]
public async Task<ActionResult<LoginTokensDto>> Login([FromBody] LoginCredentialsDto loginData)
{
    var loginTokens = await _service.Login(loginData);

    if (loginTokens == null)
        return BadRequest("Invalid credentials");

    return Ok(loginTokens);
}

// Использование кастомного типа (нельзя пользоваться методами Ok, BadRequest и т.п.)
[HttpPost("login")]
public async Task<LoginTokensDto> Login([FromBody] LoginCredentialsDto loginData)
{
    var loginTokens = await _service.Login(loginData);

    //if (loginTokens == null)
    //    return BadRequest("Invalid credentials"); // ошибка: нельзя вернуть BadRequest

    return loginTokens; // возвращаем сразу объект
}

Сервисы

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

Контроллеры должны иметь ссылку на экземпляр сервиса. Эта ссылка передается в конструктор с помощью механизма внедрения зависимости (DI).

[ApiController]
[Route("api/profile/{playerId}")]
public class ProfileController : ControllerBase
{
    private readonly IProfileService service;

    // получаем зависимость в конструкторе
    public ProfileController(IProfileService service)
    {
        service = service;
    }

    [HttpPost("test")]
    public async Task<IActionResult> Test(string playerId)
    {
        var result = await _service.SomeMethod(playerId); // используем сервис
        return Ok(result);
    }
}

Лучшие практики

  • Класс контроллера должен быть максимально чистым и в нем не должно быть никаких лишних методов кроме Action'ов.

  • Сами Action'ы также должны быть максимально чистыми и не реализовывать никакой сложной логики. Вся бизнес логика должна находиться в других проектах (Service, Repository). Задача Action'а — обработка HTTP-запросов, проверка моделей, отслеживание ошибок и возврат ответов. В большинстве случаев рекомендуется возвращать IActionResult.

  • Используйте асинхронный код. Все Action'ы должны возвращать Task<T>, методы сервисов и запросы к базе данных также должны быть по возможности асинхронными.

  • Если нужно передать список параметров, либо число передаваемых параметров большое, то вынесете их в отдельный тип и передавайте в теле запроса.

  • Разбивайте контроллеры логически на разные классы и проектируйте чистый и понятный API.

  • Для именования маршрутов и endpoint'ов используйте существительные, а не глаголы, а также используйте kebab-case (слова разделяются дефисами).


Ссылки

Tutorials Teacher — Web Api Controllers

Code Maze — ASP.NET Core Web API Best Practices


Автор документа: Артём Ветик