.NET Core WebAPI. Быстрая реализация API на базе стека Microsoft.

Привет! На волне того что тут теперь можно рассказывать на около модэкс темы, то я решил поделится с вами небольшой статьй.
На свой основной работе (благо, не студии) я занимаюсь разработкой на MODX пары достаточно крупных интернет-проектов в Республике Беларусь. Но в целом и общем я совсем не считаю себя PHP-шником и меня в принципе не особо тянуло в интерпретируемые языки. Это, конечно, мои загоны. Для себя я нашел хороший инструмент — .NET. И я начал особо плотно им интересоваться в момент анонса .NET Core, т.к Windows для меня стала слегка чужой, хотя Visual Studio до сих пор одна из тех вещей которые лично для меня являются весомым таким плюсом в пользу экосистемы языка.
Так же считаю что в .NET стеке гораздо сложнее сделать плохой код. Ни InteliSense, ни сборщик мусора, ни компилятор не даст прям намеренно сделать очень плохо. Посади 100 программистов на PHP решить задачу — получи 100 разных ее вариаций. На .NET специалисты схожего уровня напишут плюс-минус одинаково, что и позволяет C# как языку быть более поддерживаемым чтоль. Ладно, это лирика.
Так вот, время шло и на работе все что можно было сделать не на PHP я делал на .NET. И я доволен и руководители тоже. В этой статье я хочу показать что некоторые вещи делать на .NET Core крайне удобно и интересно.
Начнем с того что этот самый .NET Core неплохо было бы установить на ваш компьютер. В состав Windows издание Core до сих пор не входит, в отличии от старшего брата .NET Framework, поэтому устанавливать — придется :)
У проекта крайне няшный URL в виде dot.net и зайдя на главную вы увидите классную страницу о маркетинговым описанием проекта. При нажатии кнопки «Get Started» вас немного познакомят с синтаксисом и расскажут что .NET Core в несколько раз быстрее Node.js. Круто конечно, пусть и не совсем правда.
Если вы Виндузятник установка происходит в три клика, в случаи Linux придется ввести пару команд, в случае macOS — простая установка pkg пакета Также установить пакет можно через Homebrew. Обратите внимание что написано на странице загрузки. Для написания приложения нам понадобится SDK пакет. Если вы цените свое время, то можете установить SDK при установке IDE. Visual Studio поставит вам ее автоматически, сторонние решения в виде Rider — тоже. Visual Studio для MacOS (не дай бог конечно ее называть «вижлой») по одному богу известной причине устанавливает старую версию Core (2.1, при актуальной 2.2).
Итак!
Вы установили пакет, давайте узнаем что мы можем прямо сейчас :) Последующие действия я буду делать в консоли, да да можно и через GUI, но мы же Power User, верно? :)
  1. Для Windows:нажмите на Win+X и выберите PowerShell (работает на Windows 10 с редакции May update)
  2. Для Linux систем: заходите в вашу уютную консоль. Тоже самое нужно сделать пользователям macOS запустив ее через Spotlight или же найти ее в Launchpad.
Давайте введем
dotnet
в консоль. Что мы увидим?

Первое — мы удостоверились что .NET установлен. Второе — мы знаем у него есть help и мы можем почитать про основные команды тузлы. Подробнее я останавливаться не буду, будем следовать тому, что будем делать в данной статье.
Для создания проекта с шаблоном WebAPI нам необходимо ввести команду
dotnet new webapi
Данная команда создаст проект с шаблоном WebAPI. Обязательно почитайте какие еще шаблоны есть для создания ваших проектов.


Проект создан! По умолчанию мы видим папку с контроллерами где уже есть готовый ValueController.cs к нему мы вернемся попозже. JSON файлы — файлы конфигурации проекта (которые, кстати, уже два раза выпиливались в редакции 1.X). csproj это файл проекта. Содержит в себе информацию для вашей среды разработки (совсем грубо говоря, но большее вам знать пока и не нужно).
Давайте глянем на содержимое файла ValueController.cs

Я когда увидел в самый первый раз — у меня аж шишка задымилась. Как же тут все понятно и удобно!

Сделаем вид, что вы знаете что такое REST. Если в кратце — это управление состоянием. Если подробнее, то ставьте лукасы и я напишу большую статью по этому поводу :)
Вернемся к файлу. Для контроллера определен один общий маршрут [Route(«api/[controller]»)]. В итоге обращение api/values будет соответствовать обращению к контроллеру ValuesCotroller, причем почти ко всем действиям сразу (кроме метода Get(int id) — так как в данном случае необходим еще идентификатор, например, api/values/2).
Если вы измените название класса, например на ModxController то вместо api/values методы будут доступны по api/modx. Удобно же!
Сверху методом указаны их атрибуты, например [HttpGet]. Они указывают какой именно тип запроса будет обрабатываться методом.
Давайте разбирать все на конкретных примерах. Я возьму тривиальную задачу для выполнения для демонстрации скорости разработки.
Мы напишем небольшой To-do, прям как завещал Microsoft :)
Давайте создадим новую папку Models в нашем проекте где будем хранить модели и создадим в ней класс TodoItem. Выглядеть это должно примерно вот так:

Содержимое класса сделаем следующим:
namespace modxprodemo.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}
Что такое геттеры и сеттеры надеюсь вы в курсе.
После описания модели необходимо создать контекст базы данных. Дело в том, что мы будем использовать ORM под названием Entity Framework и для создания модели базы на основе класса (Code First, еще можно использовать Database First) ему необходим этот самый контекст.Для примера будем использовать im-memory базу данных. Полный список поддерживаемых СУБД сложно даже составить поэтому найдете себе что-то по душе (я вот, залип на MongoDB, допустим).
Создадим класс TodoContext в папке Models и приведем к следующему виду:
using Microsoft.EntityFrameworkCore;
namespace modxprodemo.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
            
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}
Надеюсь, что ваша среда разработки подсказала что вам необходимо указать using директиву вверху. Если же нет, то впишите самостоятельно.
Те кто пишет на современных фреймворках, например на Lavarel в курсе что такое DI и с чем его едят. Этот паттерн позволяет использовать IoC между классами и зависимостями. .NET Core не является исключением, и для того чтобы все заработало необходимо так же явно указать это в классе Startup.cs

Получится что-то вроде этого:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using modxprodemo.Models;
using Microsoft.EntityFrameworkCore;

namespace modxprodemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
                opt.UseInMemoryDatabase("TodoList"));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}
Модель мы описали, пришло время создать контроллер! Создадим в папке Controllers класс и назовем его TodoController. Листинг — ниже.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using modxprodemo.Models;

namespace modxprodemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoController(TodoContext context)
        {
            _context = context;

            if (_context.TodoItems.Count() == 0)
            {
                _context.TodoItems.Add(new TodoItem { Name = "Item1" });
                _context.SaveChanges();
            }
        }
    }
}
В конструкторе класса мы получаем элемент контекста который объявлен как readonly. В случае если элементов нет, то создадим один, чисто поржать. И посмотрите как элегантно и красиво можно создавать объекты в С#! Это же восхитительно красиво.
Атрибут ApiController говорит среде о том, что неплохо было бы отвечать на Web запросы. Также мы сделали инджект контекста. Контекст используется в каждом из GRUD методов в контроллере.
Но мы совсем забыли про методы! Давайте их добавим!
// GET: api/Todo
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    return await _context.TodoItems.ToListAsync();
}

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}
Эти GET методы имеют две точки:
  • Получение всех элементов по api/todo
  • Получение конкретного по api/todo/{id}
Чтобы удостовериться, давайте запустим наше приложение. Для запуска достаточно в нашей папке вызвать
dotnet run
.
Результат запуска будет следующий:

Для примера работы примера попробуем зайти по адресу localhost:/api/todo/
Получим что-то вроде:
[
  {
    "id": 1,
    "name": "Item1",
    "isComplete": false
  }
]
Вы можете попробовать обратится по заранее неизвестным параметрам и убедится что выдаст 404. При получении корректных данных вы получите код 200.
Создание Create метода будет немного сложнее, будем разбирать по кускам. Для начала листинг:
// POST: api/Todo
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem item)
{
    _context.TodoItems.Add(item);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item);
}
Тут мозг пыхера может взорваться. Какие таски? Какие async? Что это такое вообще?
И тут мы узнаем о еще одной интересной особенности C# — асинхронности. Асинхронность позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы или блоки кода. Она (асинхронность) несет выгоды в веб-приложениях при обработке запросов от пользователей, при обращении к базам данных или сетевым ресурсам. При больших запросах к базе данных асинхронный метод просто уснет на время, пока не получит данные от БД, а основной поток сможет продолжить свою работу.
Круто, не правда ли?

Прекрасно то, что .NET сам за вас понимает какие коды Location ему передавать. Чуть выше мы удостоверились что приложение отдает 200 если контент получен и 404 если не найден. Наш POST запрос вернет еще один код — 201, он же «Created». Если у вас пухнет голова от этих кодов, то я крайне рекомендую вам их выучить тут

POST метод хорош тогда, когда вы хотите создать новый элемент. Если же вы хотите исправить существующий, то логичнее будет описать PUT метод:
// PUT: api/Todo/5
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem item)
{
    if (id != item.Id)
    {
        return BadRequest();
    }

    _context.Entry(item).State = EntityState.Modified;
    await _context.SaveChangesAsync();

    return NoContent();
}
Обратите внимание, что если вы обратитесь по неверному ID то в ответе получите BadRequest, что логично. Если же все здорово, на выходе получим ошибку 204, она же No Content. Название очень стремное, но если вы молодец и выучили коды HTTP запросов что я давал вам выше, вы уже знаете что блок 2XX — это блок успешных выполнений и вам не стремно :)
По абсолютной аналогии только с кодами ошибки 404 и успеха 204 создадим последний метод — DELETE
// DELETE: api/Todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}
Для тестирования API можете использовать браузер. Или Postman. Я же на macOS использую крутое приложение — Insomnia. Не забывайте, что вы работаете с компилируемым языком и нужно постоянно собирать приложение через dotnet build и запускать через dotnet run
Большой темой будет деплой вашего приложения на сервер. ему стоит уделить чуть больше времени, думаю в рамках отдельной статьи. Но в целом на Windows все можно захостить через IIS, на macOS и Linux можно использовать встроенный веб-сервер Kestel или же родной всем нам Nginx или Apache.

Ну что же, буквально за 10 минут мы создали абсолютно полноценное REST API на C# и .NET Core которое запускается на Linux, Windows и macOS! Если вам понравится, я обязательно опишу как теперь вызвать методы API из под JS, для того чтобы вывести наше маленькое приложение на фронтэнд.
Даешь поддержку .net core на modhost.pro ?:)

В качестве послесловия хочу сказать огромное спасибо людям, что мне помогают в нашем телеграм чате MODX и терпят бывает мои идиотские вопросы. Но наше комьюнити вдохновляет меня творить, и я рад делать тоже что-то полезное для вас.

Используйте мой компонет для VSCode: vscodemodx, присылайте на пиво и в задавайте свои вопросы, если же они у вас будут. Я вам с радостью помогу.

Хорошего вам дня, комрады!
Павел Бигель
18 сентября 2019, 00:20
modx.pro
2
1 118
+15
Поблагодарить автора Отправить деньги

Комментарии: 7

Алексей Соин
18 сентября 2019, 08:03
0
Отличная статья?! Спасибо! Теперь ждем статью по REST?
    Александр Мельник
    18 сентября 2019, 09:10
    0
    Да да, про REST напишите ибо признаюсь — лично мои знания в этом вопросе на 3 с минусом.
      Иван Климчук
      18 сентября 2019, 11:22
      +1
      Это ты на golang не писал, там тебе даже дуло в ногу направить не позволят, не то, что выстрелить :)
        Павел Бигель
        18 сентября 2019, 11:26
        0
        Я пока не очень понимаю че так все по golang потекли)
          Иван Климчук
          18 сентября 2019, 11:32
          0
          Потому что простой, без лишнего мусора и сука быстрый. И еще параллельность из коробки, с которой первоклашка разберется. Ну и очень строгие правила, говнокод даже не скомпилится.
            Павел Бигель
            18 сентября 2019, 12:45
            0
            Говорят, дженериков нет)
              Иван Климчук
              18 сентября 2019, 13:11
              0
              Пока нет, но планируются. Не скажу, что без них совсем сложно. Местами, конечно, было бы удобнее, но задачи успешно решаются и без них.
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        7