Обновить
6
1.4
Роман @Gromilo

.net разработчик

Отправить сообщение

Почему моё Web API никогда не будет RESTful?

TL;DR потому мне не нужен динамический контракт.

Создатель архитектурного стиля REST Рой Филдинг — считает, что REST-архитектура должна соответствовать пяти обязательным ограничениям. У него довольно жёсткая позиция, что если API не выполняет хотя бы одного ограничения, то это не RESTful API. И тут ничего не поделаешь, как автор идеи считает, так и правильно. Далее я буду говорить, что api REST или не REST именно по Филдингу.

Но шутка в том, что хотя каждый уважающий себя бекендер знает про REST, почти никто не делает RESTful API. Но не потому что это недостижимый идеал, а потому что REST для апи, почти никому не нужен.

В смысле не нужен? А вот так, давайте взглянем на модель зрелости REST сервисов Леонарда Ричардсона. На первом и втором уровне находятся ресурсы и http-глаголы, вещь полезная, я понимаю их пользу и ничего против них не имею. А вот на третьем уровне мы видим hypermedia controls, о котором я бы хотел поговорить подробнее.

Гипермедиа как средство управления состоянием приложения или HATEOAS. Благодаря этому ограничения клиент и сервер могут развиваться независимо друг от друга, а вся необходимая информация о том, что можно делать ресурсом, содержится в ответе этого ресурса. В общем, классический сайт. Мы открываем главную или какую-нибудь другую страницу и переходим между страницами. На страницах есть ссылки на другие страницы и формы для редактирования. Вот что говорит сам автор:

«REST API следует вводить без каких-либо предварительных знаний, кроме начального URI и набора стандартных типов данных. Все переходы состояний приложения должны определяться исходя из представлений или пользовательских манипуляций с ними, полученных клиентом от сервера»

— Рой Филдинг, Архитектурные стили и проектирование сетевых архитектур программного обеспечения.

Для меня это звучит так: вот у нас сайт, соответствующий принципам REST архитектуры, тогда он может отдавать свои ресурсы в разных представлениях — например, в виде html или json. HTML — это обычная веб страница, а json содержит только данные, без визуала. Нетрудно понять, что REST клиент для json представления выглядит не очень привлекательно.

Филдинг предполагал, что сервер может менять контракт как захочет, а клиент сможет его понять на основании гипермедиа. Вы встречали таких клиентов? На самом деле они есть, но про них мало кто знает. На практике в API HATEOAS не нужен ни программисту, ни программному клиенту. Программисту нужно описание контракта, с чем прекрасно справляются OpenAPI/Swagger, желательно автогенерённые из кода. А клиенту нужен четкий контракт, как создать товар или показать ленту. И меньше всего клиент хочет, чтобы контракт менялся, и тем более не хочет поддерживать средства обнаружения и подстройки под изменённый контракт.

В итоге перед программистом встаёт дилемма:

  • Не делать HATEOAS. Но тогда его апи нельзя называть RESTful.

  • ДелатьHATEOAS. Но тогда ему нужно будет "напихать ссылок" в ответы своего апи и поддерживать их просто чтобы называться RESTful. При этом, эти ссылки никто не будет использовать.

В итоге мы живём в мире, где бэкенд часто разрабатывают с использованием принципов REST, но при этом почти не существует RESTful апи. А те, что существуют, имеют пародийное название REST-like API или pragmatic REST. А 2-й уровень зрелости REST звучит так, как будто мы остановились на полпути к идеалу. Но ведь это не идеал: 3-й уровень зрелости часто бессмысленен или даже вреден.

На практике, все насколько смирились с ситуацией, что ослабили значение термина и называют api RESTful, даже если оно только частично следует принципам REST.

А было бы круто, если бы кто-то придумал новый, хороший термин для архитектурных практик, которые бы взяли всё лучшее и полезное из REST применительно к современным Web-api. Тогда бы начинающие бэкендеры сразу осваивали актуальные подходы, а не книги Филдинга из 2000-х, как я когда-то.

Теги:
+2
Комментарии0

История небольшого бага с использованием SemaphoreSlim в C#

Где-то на сервере жил-был код:

try
{
  await semaphoreSlim.WaitAsync(cancellationToken);
  await DoSomething();
}
finally
{
  semaphoreSlim.Release();  
}

Ничего интересного: ожидаем семафор, делаем какую-то работу и по завершении освобождаем в Release.

Всё работало нормально, но в какой-то момент стали проскакивать исключенияSemaphoreFullException. Чтобы понять, когда они возникают, нужно вспомнить, как работает SemaphoreSlim.

SemaphoreSlim - имеет 2 основных параметра: текущее значение и максимальное. WaitAsync - уменьшает текущее значение на один, Release - увеличивает. Если вызывать WaitAsync, когда текущее значение равно 0, то нужно дождаться вызова Release. Если вызывать Release, когда текущее значение равно максимальному, то будет выброшено исключение SemaphoreFullException.

Получается, что Release вызывается больше раз чем WaitAsync. Как такое может быть? По коду - никак. Но, по факту, важен не сам вызов WaitAsync, а изменение текущего значения счётчика.

Всё дело в cancellationToken. Если запрошена отмена операции, то WaitAsync бросает исключение до изменения текущего значения. Далее в блоке finally исходное исключение перекрывается другим исключением, и мы теряем исходную ошибку.

Т.е. правильный код должен выглядеть вот так:

await semaphoreSlim.WaitAsync(cancellationToken);
try
{
  await DoSomething();
}
finally
{
  semaphoreSlim.Release();  
}

И невероятный фикс с перемещением строчки уезжает на тестирование.

Теги:
Всего голосов 3: ↑3 и ↓0+5
Комментарии6

Гарантировано переложить события из БД в RabbitMq streams на .net

Нужно реализовать transaction-outbox, только без дебезиума. Казалось бы ничего сложного: в цикле читаем сообщения из бд и отправляем в стрим. Если успех, удаляем сообщение из бд. Если была ошибка, берём таймаут и начинаем всё заново.

Проблема в том, что в библиотеке RabbitMQ Stream .Net Client отправка сообщения и получение результата отправки никак не связаны между собой. Не смотря на то, что список сообщений отправляется синхронно через метод Send, результат отправки можно получить только внутри коллбэк-функции ConfirmationHandler, а значит у нас трудности.

Что делать?

Решение 1: пробрасывать через метод отправки некий контекст, например добавлять к каждому сообщению ид строки из бд и удалять строку внутриConfirmationHandler. Может сработать, но оказалось, что все свойства класса Message сериализуются и уходят в стрим, а значит будет мусор. К тому же возникают сложности с управлением транзакцией, потому что непонятно когда будет удаление и требуются дополнительные синхронизации.

Решение 2: использовать TaskCompletionSource. Перед отправкой создаём новый экземпляр TaskCompletionSource, потом ожидаем результата в надежде, что библиотека отработает как надо и количество вызовов ConfirmationHandler будет совпадать с количеством вызовов Send.

Картинка, потому что лимит по символам
Картинка, потому что лимит по символам

Такое решение работает, но всё же выглядит хрупким.

Решение 3: Использовать более низкоуровневые api библиотеки, но кажется, что уйдёт куча времени на обвязку с неясным выхлопом.

Теги:
Всего голосов 1: ↑1 и ↓0+3
Комментарии2

История тупняка.
В тестах я использую билдеры на минималках: создаю DTO и мутирую его с помощью универсального метода With или специальных методов расширений. Это позволяет устанавливать только важные для теста поля. Выглядит так-то так:

var person = DefaultPerson().With(x => Name = "Вася");
//или
var person = DefaultPerson().WithName("Вася");

И вот мне понадобилось в таком стиле работать с рекорадми, а они не мутируются. Ну я такой не растерялся, вспомнил, что есть нормальная реализация паттерна builder, когда мы сначала настраиваем builder, а потом потом уже создаём интересующий нас объект. Выглядит так раньше, только есть вызов Build().

var person = DefaultPerson().WithName("Вася").Build();

Минус в том, что билдер нужно писать, поэтому вспомнил про source-generators, нашёл библиотеку в которой просто объявляешь класс билдера, вешаешь атрибут, а остальное она сделает сама. Выглядит вот так:

[AutoGenerateBuilder(typeof(PersonDto))]
public partial class PersonDtoBuilder
{
}

А потом решил загуглить как народ выкручивается, искал по запросу C# record builder. И нашёл, что можно просто использовать ключевое слово with. Очень просто:

DefaultPerson() with {Name = "Вася" }; 

И знаете что? Я знал про with, просто никогда не пользовался и забыл. Такой вот тупняк на ровном месте.

Теги:
Всего голосов 4: ↑2 и ↓2+3
Комментарии4

Информация

В рейтинге
1 539-й
Откуда
Челябинск, Челябинская обл., Россия
Зарегистрирован
Активность

Специализация

Backend Developer
Senior
C#
.NET
PostgreSQL
Git
Docker
Redis