День 2775. #Оффтоп
Сортировка I-Can’t-Believe-It-Can-Sort
Думаю, что с каждым чуть ли не ежедневно случается ситуация, когда вы написали код и думаете, что он работает, а он не работает. А бывало ли у вас наоборот?
Однажды один профессор на лекции про алгоритмы сортировки написал наивный и очевидно неверный алгоритм. Два вложенных цикла, внутри которых одно условие и смена элементов местами, если условие выполняется (некий неверный вариант пузырьковой сортировки):
Полная бессмыслица, которая, кажется, должна либо не сделать ничего, либо просто перемешать элементы. Однако позже выяснилось, что алгоритм действительно правильно сортирует элементы. Доказательство вот тут. Этот алгоритм стал популярным примером для изучения с помощью инструментов формальной верификации программ, позволяющих доказать, что подобная контринтуитивная структура действительно работает. В итоге алгоритм назвали сортировкой «Не-Могу-Поверить-Что-Это-Сортирует» (I-Can’t-Believe-It-Can-Sort).
Про него и про другие алгоритмы сортировки в новом видео Мэта Паркера.
А если вы хотите подробно изучить все алгоритмы сортировки, можно на пару часиков залипнуть вот сюда.
Сортировка I-Can’t-Believe-It-Can-Sort
Думаю, что с каждым чуть ли не ежедневно случается ситуация, когда вы написали код и думаете, что он работает, а он не работает. А бывало ли у вас наоборот?
Однажды один профессор на лекции про алгоритмы сортировки написал наивный и очевидно неверный алгоритм. Два вложенных цикла, внутри которых одно условие и смена элементов местами, если условие выполняется (некий неверный вариант пузырьковой сортировки):
for i = 0 to length - 1:
for j = 0 to length - 1:
if A[i] < A[j]:
swap(A[i], A[j])
Полная бессмыслица, которая, кажется, должна либо не сделать ничего, либо просто перемешать элементы. Однако позже выяснилось, что алгоритм действительно правильно сортирует элементы. Доказательство вот тут. Этот алгоритм стал популярным примером для изучения с помощью инструментов формальной верификации программ, позволяющих доказать, что подобная контринтуитивная структура действительно работает. В итоге алгоритм назвали сортировкой «Не-Могу-Поверить-Что-Это-Сортирует» (I-Can’t-Believe-It-Can-Sort).
Про него и про другие алгоритмы сортировки в новом видео Мэта Паркера.
А если вы хотите подробно изучить все алгоритмы сортировки, можно на пару часиков залипнуть вот сюда.
👍5
День 2776. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
45. Проверка работоспособности и мониторинг
«Как бы вы реализовали проверку работоспособности и мониторинг .NET-сервиса? Опишите инструменты и методы, которые вы бы использовали для обеспечения надёжного управления работоспособностью приложения».
Хороший ответ
Реализация проверки работоспособности и мониторинга включает в себя настройку конечных точек, к которым могут обращаться балансировщики нагрузки или инструменты мониторинга для проверки состояния приложения.
Можно добавить и настроить проверки работоспособности, используя встроенные функции ASP.NET Core:
Этот фрагмент кода настраивает простую проверку работоспособности, которая всегда возвращает «Healthy». Можно заменить логику проверки реальными тестами, вроде проверки подключения к БД или доступности внешних зависимостей.
Для более сложных сценариев можно добавить проверки для конкретных сервисов, таких как базы данных, серверы кэширования или API, от которых зависит приложение, например, вот проверка доступности базы:
Необходимо убедиться, что конечные точки проверки работоспособности хорошо документированы и доступны для соответствующих инструментов мониторинга, но защищены от несанкционированного доступа.
Важно добавить логирование проверок работоспособности для регистрации сбоев или изменений в поведении системы. Это может помочь в диагностике проблем, приводящих к сбоям.
Преимущества
- Проактивный мониторинг: позволяет команде обнаруживать проблемы и реагировать на них до того, как они повлияют на пользователей.
- Наблюдаемость: обеспечивает прозрачность состояния приложения и помогает поддерживать его надёжность и производительность.
- Переключение при сбоях и высокая доступность: обеспечивает автоматическое переключение при сбоях проверок работоспособности в облачных средах.
Часто встречающийся плохой ответ
Важно правильно логировать сообщения об ошибках и убедиться, что приложение автоматически перезапускается в случае сбоя. Этого должно быть достаточно для поддержания его работы.
Почему это неправильно:
- Отсутствие проактивного мониторинга: полагаться исключительно на журналы ошибок и автоматические перезапуски не предотвращает сбои, а лишь реагирует после их возникновения, что может привести к простоям.
- Игнорирование преимуществ проверок работоспособности, которые могут отслеживать состояние приложения в режиме реального времени и предоставлять ранние предупреждения о потенциальных проблемах.
- Неадекватные стратегии отказоустойчивости: автоматические перезапуски могут не устранять основные проблемы и приводить к повторным сбоям без надлежащей диагностики или решения.
Эта ошибка часто возникает из-за непонимания возможностей и важности проверок работоспособности и мониторинга в современных архитектурах приложений, возможно, из-за недостатка опыта работы в средах, где высокая доступность и надёжность имеют решающее значение.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
45. Проверка работоспособности и мониторинг
«Как бы вы реализовали проверку работоспособности и мониторинг .NET-сервиса? Опишите инструменты и методы, которые вы бы использовали для обеспечения надёжного управления работоспособностью приложения».
Хороший ответ
Реализация проверки работоспособности и мониторинга включает в себя настройку конечных точек, к которым могут обращаться балансировщики нагрузки или инструменты мониторинга для проверки состояния приложения.
Можно добавить и настроить проверки работоспособности, используя встроенные функции ASP.NET Core:
var builder = WebApplication.CreateBuilder(args);
// Добавляем проверки
builder.Services.AddHealthChecks()
.AddCheck("Sample Health Check", () =>
HealthCheckResult.Healthy("OK"));
var app = builder.Build();
// Конечная точка
app.MapHealthChecks("/health");
app.Run();
Этот фрагмент кода настраивает простую проверку работоспособности, которая всегда возвращает «Healthy». Можно заменить логику проверки реальными тестами, вроде проверки подключения к БД или доступности внешних зависимостей.
Для более сложных сценариев можно добавить проверки для конкретных сервисов, таких как базы данных, серверы кэширования или API, от которых зависит приложение, например, вот проверка доступности базы:
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();
Необходимо убедиться, что конечные точки проверки работоспособности хорошо документированы и доступны для соответствующих инструментов мониторинга, но защищены от несанкционированного доступа.
Важно добавить логирование проверок работоспособности для регистрации сбоев или изменений в поведении системы. Это может помочь в диагностике проблем, приводящих к сбоям.
Преимущества
- Проактивный мониторинг: позволяет команде обнаруживать проблемы и реагировать на них до того, как они повлияют на пользователей.
- Наблюдаемость: обеспечивает прозрачность состояния приложения и помогает поддерживать его надёжность и производительность.
- Переключение при сбоях и высокая доступность: обеспечивает автоматическое переключение при сбоях проверок работоспособности в облачных средах.
Часто встречающийся плохой ответ
Важно правильно логировать сообщения об ошибках и убедиться, что приложение автоматически перезапускается в случае сбоя. Этого должно быть достаточно для поддержания его работы.
Почему это неправильно:
- Отсутствие проактивного мониторинга: полагаться исключительно на журналы ошибок и автоматические перезапуски не предотвращает сбои, а лишь реагирует после их возникновения, что может привести к простоям.
- Игнорирование преимуществ проверок работоспособности, которые могут отслеживать состояние приложения в режиме реального времени и предоставлять ранние предупреждения о потенциальных проблемах.
- Неадекватные стратегии отказоустойчивости: автоматические перезапуски могут не устранять основные проблемы и приводить к повторным сбоям без надлежащей диагностики или решения.
Эта ошибка часто возникает из-за непонимания возможностей и важности проверок работоспособности и мониторинга в современных архитектурах приложений, возможно, из-за недостатка опыта работы в средах, где высокая доступность и надёжность имеют решающее значение.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍6
День 2777. #ЗаметкиНаПолях
Конечные Точки в ASP.NET Core не Имеют Таймаута
ASP.NET Core по умолчанию не применяет таймаут приложения к входящим запросам. Встроенное промежуточное ПО для таймаута запроса добавляет дедлайн, но вызывает только HttpContext.RequestAborted. Конечная точка должна передать этот токен в операцию, которую вы хотите остановить.
Медленный запрос к БД или зависший вызов API могут продолжать потреблять ресурсы даже после того, как ответ перестанет быть полезным. В .NET 8 было введено промежуточное ПО для таймаута запроса, чтобы обеспечить кооперативный дедлайн обработки.
Зарегистрируем промежуточное ПО и установим тайм-аут в Program.cs:
-
-
- Через 3 секунды
* Тестируйте это без отладчика, т.к. при подключенном отладчике таймаут не срабатывает.
Токен должен достичь отменяемой работы
Промежуточное ПО таймаута не прерывает поток и не вызывает HttpContext.Abort(). Оно отменяет токен и продолжает ждать конечную точку.
Удалите
То же правило применяется и к реальным зависимостям. Передавайте токен через сервисы приложения в EF Core:
EF Core пересылает токен провайдеру БД, который решает, можно ли отменить операцию в БД. Токен должен пройти по всей цепочке вызовов, прежде чем провайдер сможет его увидеть (см. также: ошибки при передаче токена отмены). Передавайте токен в HttpClient, клиенты обмена сообщениями и другие асинхронные операции, где безопасно отказаться от операции.
Выберите таймауты для каждой конечной точки
Не все конечные точки должны иметь одинаковый лимит. Небольшое чтение из API и экспорт отчёта имеют разные дедлайны, поэтому задайте для них разные политики:
Для событий Server-Sent, Web-сокетов, long pooling и больших загрузок обычно требуется более длительная политика таймаутов или метод
Итого
Ошибка 504 от промежуточного ПО таймаута означает, что отмена достигла его. Это не значит, что все нижележащие операции были остановлены. Передавайте токен отмены во все нижележащие операции. Используйте логи или трассировку, чтобы убедиться, что зависимости обработали отмену.
Источник: https://milanjovanovic.tech/blog/your-aspnetcore-endpoints-dont-have-a-timeout
Конечные Точки в ASP.NET Core не Имеют Таймаута
ASP.NET Core по умолчанию не применяет таймаут приложения к входящим запросам. Встроенное промежуточное ПО для таймаута запроса добавляет дедлайн, но вызывает только HttpContext.RequestAborted. Конечная точка должна передать этот токен в операцию, которую вы хотите остановить.
Медленный запрос к БД или зависший вызов API могут продолжать потреблять ресурсы даже после того, как ответ перестанет быть полезным. В .NET 8 было введено промежуточное ПО для таймаута запроса, чтобы обеспечить кооперативный дедлайн обработки.
Зарегистрируем промежуточное ПО и установим тайм-аут в Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts();
var app = builder.Build();
app.UseRequestTimeouts();
app.MapGet("/reports", async (
CancellationToken ct) =>
{
await Task.Delay(TimeSpan.FromSeconds(10), ct);
return Results.Ok("Ready");
})
.WithRequestTimeout(TimeSpan.FromSeconds(3));
app.Run();
-
AddRequestTimeouts только регистрирует необходимые сервисы, но не устанавливает ограничение.-
WithRequestTimeout задаёт для конечной точки 3-секундный таймаут. Минимальные API привязывают параметр CancellationToken к HttpContext.RequestAborted.- Через 3 секунды
Task.Delay обнаруживает отмену и генерирует исключение. Если это исключение достигает промежуточного ПО до начала ответа, ответ по умолчанию — пустой 504 Gateway Timeout.* Тестируйте это без отладчика, т.к. при подключенном отладчике таймаут не срабатывает.
Токен должен достичь отменяемой работы
Промежуточное ПО таймаута не прерывает поток и не вызывает HttpContext.Abort(). Оно отменяет токен и продолжает ждать конечную точку.
Удалите
ct из вызова Task.Delay выше, и обработчик будет ждать полные 10 секунд, прежде чем вернуть 200 OK. Ошибка 504 не возникает, т.к. исключение отмены не достигает промежуточного ПО.То же правило применяется и к реальным зависимостям. Передавайте токен через сервисы приложения в EF Core:
public Task<Order?> GetByIdAsync(
Guid id,
CancellationToken ct)
{
return dbContext.Orders
.AsNoTracking()
.SingleOrDefaultAsync(
order => order.Id == id,
ct);
}
EF Core пересылает токен провайдеру БД, который решает, можно ли отменить операцию в БД. Токен должен пройти по всей цепочке вызовов, прежде чем провайдер сможет его увидеть (см. также: ошибки при передаче токена отмены). Передавайте токен в HttpClient, клиенты обмена сообщениями и другие асинхронные операции, где безопасно отказаться от операции.
Выберите таймауты для каждой конечной точки
Не все конечные точки должны иметь одинаковый лимит. Небольшое чтение из API и экспорт отчёта имеют разные дедлайны, поэтому задайте для них разные политики:
builder.Services.AddRequestTimeouts(opts =>
{
opts.AddPolicy("short", TimeSpan.FromSeconds(3));
opts.AddPolicy("long", TimeSpan.FromSeconds(30));
});
app.MapGet("/orders/{id:guid}", GetOrder)
.WithRequestTimeout("short");
app.MapGet("/reports/{id:guid}", ExportReport)
.WithRequestTimeout("long");
app.MapGet("/events", StreamEvents)
.DisableRequestTimeout();
Для событий Server-Sent, Web-сокетов, long pooling и больших загрузок обычно требуется более длительная политика таймаутов или метод
.DisableRequestTimeout().Итого
Ошибка 504 от промежуточного ПО таймаута означает, что отмена достигла его. Это не значит, что все нижележащие операции были остановлены. Передавайте токен отмены во все нижележащие операции. Используйте логи или трассировку, чтобы убедиться, что зависимости обработали отмену.
Источник: https://milanjovanovic.tech/blog/your-aspnetcore-endpoints-dont-have-a-timeout
День 2778. #ЗаметкиНаПолях
Потоковая Передача JSON в .NET. Начало
Допустим, у вас есть конечная точка, которая возвращает список: все заказы за последний квартал или поток показаний датчиков. Код выглядит нормально:
Для пары сотен строк это не заметно. При паре десятков тысяч происходит скачок потребления памяти, начинает работать сборщик мусора, и время до получения первого байта растёт, как и нагрузка на процесс при увеличении количества одновременных запросов. Решение в том, чтобы прекратить буферизацию. Сериализуем элемент, отдаём, переходим к следующему.
System.Text.Json может передавать IAsyncEnumerable<T> потоком начиная с .NET 6. Платформа выдаёт JSON-массив потоком, а не буферизирует его:
Это решает проблему использования памяти. Сервер хранит один заказ за раз, а не весь список. Вывод – JSON-массив:
Это вполне приемлемо для браузера, вызывающего функцию
Окончание следует…
Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines
Потоковая Передача JSON в .NET. Начало
Допустим, у вас есть конечная точка, которая возвращает список: все заказы за последний квартал или поток показаний датчиков. Код выглядит нормально:
app.MapGet("/orders/export",
async (OrderService service) =>
{
List<Order> orders = await service.GetAllAsync();
return Results.Ok(orders);
});GetAllAsync извлекает все строки в List<Order>. Затем Results.Ok передаёт весь список сериализатору, который формирует JSON-массив в памяти. Т.е. вы храните две копии данных одновременно — объекты и их сериализованную форму, — а клиент ждёт, пока не будет сериализована последняя строка, прежде чем что-либо получить.Для пары сотен строк это не заметно. При паре десятков тысяч происходит скачок потребления памяти, начинает работать сборщик мусора, и время до получения первого байта растёт, как и нагрузка на процесс при увеличении количества одновременных запросов. Решение в том, чтобы прекратить буферизацию. Сериализуем элемент, отдаём, переходим к следующему.
System.Text.Json может передавать IAsyncEnumerable<T> потоком начиная с .NET 6. Платформа выдаёт JSON-массив потоком, а не буферизирует его:
// возвращает IAsyncEnumerable<Order>
app.MapGet("/orders/export", (OrderService service) =>
service.GetAllAsyncStream());
…
public async IAsyncEnumerable<Order>
GetAllAsyncStream(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var order in _repo.ReadAllAsync(ct))
yield return order;
}
Это решает проблему использования памяти. Сервер хранит один заказ за раз, а не весь список. Вывод – JSON-массив:
[{"Id":1,"Total":42.0},{"Id":2,"Total":19.5}, …]Это вполне приемлемо для браузера, вызывающего функцию
fetch и использующего await res.json(). Но есть недостаток для больших объёмов данных: массив представляет собой один JSON-документ. Потребитель, желающий обрабатывать элементы по мере их поступления, должен разбирать массив постепенно, и, если соединение обрывается на полпути, остается усечённый, некорректный JSON-документ — завершающий символ ] не доходит. Добавить к нему данные тоже невозможно. Для потоковой обработки, записи в лог или экспорта данных JSON-массив имеет неправильный формат.Окончание следует…
Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines
👍6
День 2779. #ЧтоНовенького #NET11
Потоковая Передача JSON в .NET 11. Окончание
Начало
JSON-строки: один объект на строку
JSON-строки (или NDJSON - Newline Delimited JSON) — формат, который уже используется большинством инструментов потоковой обработки данных — одно значение JSON на строку, разделённые символом
Каждая строка представляет собой полный, независимый JSON-документ. Потребитель читает строку, разбирает её, обрабатывает и забывает о ней. Если соединение обрывается после второй строки, первые две строки остаются действительными и пригодными для использования. Вы можете добавить четвёртую строку в файл, не затрагивая первые три. Конвейеры обработки, логи, загрузчики данных, потоки событий используют этот формат.
В .NET 11 System.Text.Json может создавать его напрямую. Новые перегрузки JsonSerializer.SerializeAsyncEnumerable принимают флаг topLevelValues:
При использовании
Потоковая передача NDJSON из конечной точки
В ASP.NET Core результат по умолчанию сериализуется в JSON-массив, поэтому для отправки JSON-строк нужно самостоятельно записывать данные в поток ответа:
SerializeAsyncEnumerable возвращает Task, поэтому конечная точка просто возвращает его. Заказы поступают из БД через сериализатор по одному. Память остаётся неизменной независимо от того, экспортируется 100 строк или 10 миллионов. Используйте тип содержимого
Чтение NDJSON
Чтение работает в любой версии .NET, т.к. строка представляет собой обычный JSON:
Вы обрабатываете каждую запись по мере её поступления и не создаёте большую коллекцию.
Когда использовать?
Когда данные большие или неопределённого размера: экспорт большой таблицы, возврат длинного отчёта, подача данных в конвейер обработки или запись лога или файла событий с возможностью добавления. Формат особенно эффективен, когда потребитель обрабатывает записи по одной и когда разорванное соединение должно оставлять после себя действительные частичные данные.
Небольшие ответы прекрасно поместятся в память, а обычный JSON-массив браузеры и большинство HTTP-клиентов ожидают по умолчанию. Переход на JSON-строки в этом случае только усложнит обработку ответа без каких-либо преимуществ.
FAQ
1. В чем разница между JSON-строками и NDJSON?
Это один и тот же формат. "NDJSON" (Newline Delimited JSON) и "JSON Lines" (JSONL) — два его названия, а
2. Загружает ли SerializeAsyncEnumerable всю коллекцию в память?
Нет. Он перебирает элементы IAsyncEnumerable<T> по одному, сериализует каждый и записывает его в поток вывода, прежде чем перейти к следующему. Это обеспечивает стабильность использованной памяти независимо от количества передаваемых элементов.
3. Нужен ли NDJSON для потоковой передачи, или достаточно IAsyncEnumerable?
Возвращение IAsyncEnumerable<T> уже обеспечивает потоковую передачу JSON-массива без буферизации, поэтому управление памятью происходит в любом случае. JSON-строки добавляют преимущества формата: каждая запись является независимой, частичный вывод при разрыве соединения всё равно валиден, и можно дописывать данные в файл.
4. Могут ли браузеры читать ответ NDJSON?
Автоматически – нет. Если используется
Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines
Потоковая Передача JSON в .NET 11. Окончание
Начало
JSON-строки: один объект на строку
JSON-строки (или NDJSON - Newline Delimited JSON) — формат, который уже используется большинством инструментов потоковой обработки данных — одно значение JSON на строку, разделённые символом
\n:{"Id":1,"Total":42.0}
{"Id":2,"Total":19.5}
{"Id":3,"Total":88.25}Каждая строка представляет собой полный, независимый JSON-документ. Потребитель читает строку, разбирает её, обрабатывает и забывает о ней. Если соединение обрывается после второй строки, первые две строки остаются действительными и пригодными для использования. Вы можете добавить четвёртую строку в файл, не затрагивая первые три. Конвейеры обработки, логи, загрузчики данных, потоки событий используют этот формат.
В .NET 11 System.Text.Json может создавать его напрямую. Новые перегрузки JsonSerializer.SerializeAsyncEnumerable принимают флаг topLevelValues:
using System.Text;
using System.Text.Json;
static async IAsyncEnumerable<Reading> GetReadings()
{
yield return new("sensor-1", 21.5);
yield return new("sensor-2", 22.0);
}
await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsyncEnumerable(
stream,
GetReadings(),
topLevelValues: true);
Console.WriteLine(
Encoding.UTF8.GetString(stream.ToArray()));
// {"Id":"sensor-1","Value":21.5}
// {"Id":"sensor-2","Value":22}
public sealed record Reading(string Id, double Value);
При использовании
topLevelValues: true отсутствуют открывающая и закрывающая квадратные скобки и запятые между элементами. Каждый элемент сериализуется и сопровождается переводом строки. Формат также игнорирует WriteIndented, поэтому каждый объект остается на отдельной строке.Потоковая передача NDJSON из конечной точки
В ASP.NET Core результат по умолчанию сериализуется в JSON-массив, поэтому для отправки JSON-строк нужно самостоятельно записывать данные в поток ответа:
app.MapGet("/orders/export",
(OrderService service,
HttpResponse response,
CancellationToken ct) =>
{
response.ContentType = "application/x-ndjson";
return JsonSerializer.SerializeAsyncEnumerable(
response.Body,
service.GetAllAsyncStream(ct),
topLevelValues: true);
});SerializeAsyncEnumerable возвращает Task, поэтому конечная точка просто возвращает его. Заказы поступают из БД через сериализатор по одному. Память остаётся неизменной независимо от того, экспортируется 100 строк или 10 миллионов. Используйте тип содержимого
application/x-ndjson (или application/jsonl), чтобы клиенты знали, что они получают, вместо того чтобы предполагать наличие единого массива.Чтение NDJSON
Чтение работает в любой версии .NET, т.к. строка представляет собой обычный JSON:
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
if (line.Length == 0) continue;
var reading =
JsonSerializer.Deserialize<Reading>(line)!;
await ProcessAsync(reading);
}
Вы обрабатываете каждую запись по мере её поступления и не создаёте большую коллекцию.
Когда использовать?
Когда данные большие или неопределённого размера: экспорт большой таблицы, возврат длинного отчёта, подача данных в конвейер обработки или запись лога или файла событий с возможностью добавления. Формат особенно эффективен, когда потребитель обрабатывает записи по одной и когда разорванное соединение должно оставлять после себя действительные частичные данные.
Небольшие ответы прекрасно поместятся в память, а обычный JSON-массив браузеры и большинство HTTP-клиентов ожидают по умолчанию. Переход на JSON-строки в этом случае только усложнит обработку ответа без каких-либо преимуществ.
FAQ
1. В чем разница между JSON-строками и NDJSON?
Это один и тот же формат. "NDJSON" (Newline Delimited JSON) и "JSON Lines" (JSONL) — два его названия, а
application/x-ndjson — это тип содержимого, который вы будете встречать чаще всего.2. Загружает ли SerializeAsyncEnumerable всю коллекцию в память?
Нет. Он перебирает элементы IAsyncEnumerable<T> по одному, сериализует каждый и записывает его в поток вывода, прежде чем перейти к следующему. Это обеспечивает стабильность использованной памяти независимо от количества передаваемых элементов.
3. Нужен ли NDJSON для потоковой передачи, или достаточно IAsyncEnumerable?
Возвращение IAsyncEnumerable<T> уже обеспечивает потоковую передачу JSON-массива без буферизации, поэтому управление памятью происходит в любом случае. JSON-строки добавляют преимущества формата: каждая запись является независимой, частичный вывод при разрыве соединения всё равно валиден, и можно дописывать данные в файл.
4. Могут ли браузеры читать ответ NDJSON?
Автоматически – нет. Если используется
await res.json() — он ожидает один JSON-документ. Браузер должен читать поток ответа и разделять его по символам новой строки, самостоятельно разбирая каждую строку. Для стандартного запроса данных из браузера обычный массив проще; JSON-строки следует использовать для конвейеров обработки и экспорта больших объёмов данных.Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines
👍10
День 2780. #ЧтоНовенького #VS
Избегаем Путаницы при Переключении Между Окнами Visual Studio
Если вы запускаете несколько экземпляров Visual Studio одновременно для нескольких решений, все они выглядят одинаково. Бывало ли, что вы переключались между окнами и начинали работать не в том? Было бы здорово, если бы для каждого решения можно было задать свою цветовую тему. Эта функция уже реализована, и не только в отношении цвета.
Откройте меню Tools > Options (Инструменты > Параметры). В верхней части окна найдите выпадающий список Applies to (Применить к) и переключите его с профиля пользователя на текущее решение (Current solution). С этого момента любые изменения настроек будут применяться только к открытому решению. Если вы закроете его и откроете позже снова, тема сохранится. А при открытии другого решения вы увидите назначенную для него цветовую схему. Выбирайте цвета с заметным контрастом для решений, с которыми работаете одновременно. Например: темная тема для сервиса, синяя — для клиентской части. Цель — различать их мгновенно, не вчитываясь в заголовок окна.
Выпадающий список Applies to — не просто функция для выбора цвета. Это модель определения области действия в новой системе настроек, а цветовая тема — лишь самый наглядный пример того, как эту модель можно использовать. Новая система объединяет настройки в единый согласованный интерфейс с возможностью поиска и поддержкой формата JSON.
Таким образом, главное нововведение заключается в том, что настройки теперь имеют область действия и привязаны к файлу, а файл можно добавить в систему контроля версий. Настройки, привязанные к конкретному решению, хранятся в файле
Важный нюанс
Настройки уровня решения хранятся отдельно от общих пользовательских настроек, поэтому изменение параметров для конкретного проекта не затронет конфигурацию, которую вы используете повсеместно. Чтобы изменить настройки по умолчанию для всех решений, просто переключите выпадающий список Applies to обратно на ваш профиль пользователя.
Такое разделение позволяет безопасно экспериментировать. Можно задать для одного решения особую тему оформления, проверить, удобно ли с ней работать, и, если результат не понравится, просто удалить настройку уровня решения. Visual Studio автоматически вернётся к вашим пользовательским настройкам — восстанавливать их вручную не придется.
Если хочется большего
Задолго до появления этой функции ту же задачу решало расширение Solution Colors, но с иным подходом. Оно работает только с цветами и обеспечивает более тонкую настройку: вместо смены всей темы оно окрашивает отдельные элементы IDE в цвета, специфичные для конкретного решения. При этом цвет может подбираться автоматически, так что вам даже не придётся ничего решать самостоятельно. Если вам нужен более тонкий контроль над тем, какие именно элементы меняют цвет, это расширение по-прежнему доступно и заслуживает внимания.
Источник: https://devblogs.microsoft.com/visualstudio/stop-alt-tabbing-into-the-wrong-visual-studio/
Избегаем Путаницы при Переключении Между Окнами Visual Studio
Если вы запускаете несколько экземпляров Visual Studio одновременно для нескольких решений, все они выглядят одинаково. Бывало ли, что вы переключались между окнами и начинали работать не в том? Было бы здорово, если бы для каждого решения можно было задать свою цветовую тему. Эта функция уже реализована, и не только в отношении цвета.
Откройте меню Tools > Options (Инструменты > Параметры). В верхней части окна найдите выпадающий список Applies to (Применить к) и переключите его с профиля пользователя на текущее решение (Current solution). С этого момента любые изменения настроек будут применяться только к открытому решению. Если вы закроете его и откроете позже снова, тема сохранится. А при открытии другого решения вы увидите назначенную для него цветовую схему. Выбирайте цвета с заметным контрастом для решений, с которыми работаете одновременно. Например: темная тема для сервиса, синяя — для клиентской части. Цель — различать их мгновенно, не вчитываясь в заголовок окна.
Выпадающий список Applies to — не просто функция для выбора цвета. Это модель определения области действия в новой системе настроек, а цветовая тема — лишь самый наглядный пример того, как эту модель можно использовать. Новая система объединяет настройки в единый согласованный интерфейс с возможностью поиска и поддержкой формата JSON.
Таким образом, главное нововведение заключается в том, что настройки теперь имеют область действия и привязаны к файлу, а файл можно добавить в систему контроля версий. Настройки, привязанные к конкретному решению, хранятся в файле
settings.VisualStudio.json в корневой папке решения — прямо рядом с файлом .sln или .slnx. Можно добавить этот файл в систему контроля версий, и каждый, кто клонирует репозиторий, получит те же настройки. Новый коллега откроет решение, и IDE сразу будет выглядеть и вести себя так, как договорилась команда, — без необходимости изучать инструкции по настройке: «Перейдите в меню Tools > Options и измените вот эти девять параметров». Не хотите навязывать свои предпочтения остальным? Добавьте файл в .gitignore, и настройки останутся только вашими.Важный нюанс
Настройки уровня решения хранятся отдельно от общих пользовательских настроек, поэтому изменение параметров для конкретного проекта не затронет конфигурацию, которую вы используете повсеместно. Чтобы изменить настройки по умолчанию для всех решений, просто переключите выпадающий список Applies to обратно на ваш профиль пользователя.
Такое разделение позволяет безопасно экспериментировать. Можно задать для одного решения особую тему оформления, проверить, удобно ли с ней работать, и, если результат не понравится, просто удалить настройку уровня решения. Visual Studio автоматически вернётся к вашим пользовательским настройкам — восстанавливать их вручную не придется.
Если хочется большего
Задолго до появления этой функции ту же задачу решало расширение Solution Colors, но с иным подходом. Оно работает только с цветами и обеспечивает более тонкую настройку: вместо смены всей темы оно окрашивает отдельные элементы IDE в цвета, специфичные для конкретного решения. При этом цвет может подбираться автоматически, так что вам даже не придётся ничего решать самостоятельно. Если вам нужен более тонкий контроль над тем, какие именно элементы меняют цвет, это расширение по-прежнему доступно и заслуживает внимания.
Источник: https://devblogs.microsoft.com/visualstudio/stop-alt-tabbing-into-the-wrong-visual-studio/
👍9
День 2781. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Начало
Представим эндпоинт, принимающий файлы, изменяющий их размер и возвращающий ответ. В демо-версии всё работает отлично. Но в проде нагрузка растёт, и эндпойнт получает сотни запросов в секунду. Каждый запрос запускает задачу по изменению размера прямо в потоке обработки; CPU загружается до 100%, а остальные функции приложения начинают завершаться по таймауту, т.к. пул потоков перегружен.
Не обязательно обрабатывать запросы сразу. Достаточно принять данные и обработать их в удобном для системы темпе. Это классическая задача типа «производитель-потребитель»: одна сторона передает задачу, а другая выполняет её со скоростью, которую реально может поддерживать. Для простейшей её реализации не нужны брокеры сообщений, достаточно System.Threading.Channels. Далее рассмотрим реализацию.
Каналы в .NET позволяют передавать данные между производителями и потребителями, работающими параллельно в одном процессе. Производители записывают данные в
Почему «наивное» решение только всё усугубляет
Первый порыв в решении проблемы – сделать всё асинхронным: запустить задачу через
Такой подход обеспечивает быстрый отклик, поэтому кажется удачным решением. Но это не так. Количество запускаемых задач ничем не ограничено, поэтому всплеск нагрузки, который раньше «вешал» CPU, делает это снова — при этом всем отправляется ответ
Правильное решение — использовать очередь с ограничением размера между двумя компонентами. Когда очередь заполняется, отправитель вынужден ждать; это ожидание служит сигналом о том, что задачи поступают быстрее, чем система успевает их обрабатывать, — и именно эту информацию важно выявлять, а не скрывать.
Основы работы с каналами
У
Режим переполнения (FullMode) — ключевой элемент:
- Wait —
- DropWrite — молча отбрасывает записываемый элемент;
- DropOldest — заменяет самый старый элемент из очереди входящим;
- DropNewest — заменяет самый новый элемент из очереди входящим.
Режим Wait подходит, когда важен каждый элемент и лучше замедлить работу производителя, чем потерять данные. Режимы Drop* предназначены для телеметрии и потоков данных в реальном времени, где свежий элемент важнее полной истории: например, при передаче метрик отбросить самое старое показание вполне допустимо.
Параметры SingleReader и SingleWriter служат для оптимизации. Устанавливайте их в
Запросы поступают из множества потоков и записываются в один канал. Опустошением канала занимается небольшой фиксированный пул потребителей. Когда канал переполняется, операция записи приостанавливается, и сигнал обратного давления передаётся непосредственно вызывающему коду — именно это и требуется, так как замедление становится явным и предсказуемым.
Продолжение следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
Паттерн «Производитель-потребитель» c System.Threading.Channels. Начало
Представим эндпоинт, принимающий файлы, изменяющий их размер и возвращающий ответ. В демо-версии всё работает отлично. Но в проде нагрузка растёт, и эндпойнт получает сотни запросов в секунду. Каждый запрос запускает задачу по изменению размера прямо в потоке обработки; CPU загружается до 100%, а остальные функции приложения начинают завершаться по таймауту, т.к. пул потоков перегружен.
Не обязательно обрабатывать запросы сразу. Достаточно принять данные и обработать их в удобном для системы темпе. Это классическая задача типа «производитель-потребитель»: одна сторона передает задачу, а другая выполняет её со скоростью, которую реально может поддерживать. Для простейшей её реализации не нужны брокеры сообщений, достаточно System.Threading.Channels. Далее рассмотрим реализацию.
Каналы в .NET позволяют передавать данные между производителями и потребителями, работающими параллельно в одном процессе. Производители записывают данные в
ChannelWriter<T>, потребители считывают их из ChannelReader<T>, а канал обеспечивает потокобезопасную асинхронную передачу между ними. Важный момент — канал с ограниченным размером (bounded) автоматически обеспечивает механизм «обратного давления» (backpressure).Почему «наивное» решение только всё усугубляет
Первый порыв в решении проблемы – сделать всё асинхронным: запустить задачу через
Task.Run и сразу вернуть ответ:[HttpPost("process")]
public IActionResult Process(UploadRequest request)
{
// Запустил и забыл. Выглядит асинхронно
_ = Task.Run(() => _imgService.Resize(request));
return Accepted();
}Такой подход обеспечивает быстрый отклик, поэтому кажется удачным решением. Но это не так. Количество запускаемых задач ничем не ограничено, поэтому всплеск нагрузки, который раньше «вешал» CPU, делает это снова — при этом всем отправляется ответ
202, сигнализирующий об успешном принятии запроса. Если происходит перезапуск процесса, эта работа бесследно исчезает. А т.к. за задачами никто не следит, возникающие в них исключения просто пропадают. В итоге вы меняете явное замедление системы на скрытую потерю огромного объёма работы.Правильное решение — использовать очередь с ограничением размера между двумя компонентами. Когда очередь заполняется, отправитель вынужден ждать; это ожидание служит сигналом о том, что задачи поступают быстрее, чем система успевает их обрабатывать, — и именно эту информацию важно выявлять, а не скрывать.
Основы работы с каналами
У
Channel<T> есть два конца. Вы создаёте его и передаёте каждый конец соответствующей стороне:using System.Threading.Channels;
// Вместимость 100. При заполнении писатели ждут
var ch = Channel.CreateBounded<WorkItem>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = false,
SingleWriter = false
});
ChannelWriter<WorkItem> writer = ch.Writer;
ChannelReader<WorkItem> reader = ch.Reader;
Режим переполнения (FullMode) — ключевой элемент:
- Wait —
WriteAsync ждёт появления свободного места (по умолчанию);- DropWrite — молча отбрасывает записываемый элемент;
- DropOldest — заменяет самый старый элемент из очереди входящим;
- DropNewest — заменяет самый новый элемент из очереди входящим.
Режим Wait подходит, когда важен каждый элемент и лучше замедлить работу производителя, чем потерять данные. Режимы Drop* предназначены для телеметрии и потоков данных в реальном времени, где свежий элемент важнее полной истории: например, при передаче метрик отбросить самое старое показание вполне допустимо.
Параметры SingleReader и SingleWriter служат для оптимизации. Устанавливайте их в
true, только когда у вас действительно ровно один читатель или один писатель — так канал использует более быстрый внутренний путь обработки. Если сомневаетесь, оставляйте false: ошибочный true может привести к трудноуловимому состоянию гонки.Запросы поступают из множества потоков и записываются в один канал. Опустошением канала занимается небольшой фиксированный пул потребителей. Когда канал переполняется, операция записи приостанавливается, и сигнал обратного давления передаётся непосредственно вызывающему коду — именно это и требуется, так как замедление становится явным и предсказуемым.
Продолжение следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
👍5
День 2782. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Продолжение
Начало
Производитель: конечная точка, передающая задачи
Производитель просто выполняет запись. Единственный важный нюанс — как поступить, если канал переполнен; метод
Если при перегрузке системы вы предпочитаете отклонять запросы (что правильнее публичного API, для предотвращения атак), используйте
TryWrite никогда не блокирует выполнение. Он возвращает false сразу же, как только канал оказывается заполненным, что позволяет преобразовать эту ситуацию в чёткий ответ 429 вместо ожидания. Выбор стратегии зависит от того, кто инициирует вызов: если это внутренняя пакетная задача, можно позволить ей подождать, а если публичная конечная точка — лучше сразу отклонить запрос.
Потребитель: сервис, считывающий данные из канала
Потребитель реализован в виде BackgroundService, т.е. запускается и останавливается вместе с приложением. Весь цикл обработки умещается в одну строку благодаря методу ReadAllAsync, который выдаёт элементы до тех пор, пока канал не будет закрыт:
Замечания:
-
-
Окончание следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
Паттерн «Производитель-потребитель» c System.Threading.Channels. Продолжение
Начало
Производитель: конечная точка, передающая задачи
Производитель просто выполняет запись. Единственный важный нюанс — как поступить, если канал переполнен; метод
WriteAsync берёт это на себя: он завершается немедленно, если есть свободное место, или ожидает, пока потребитель освободит слот.[ApiController]
[Route("api/[controller]")]
public class ProcessController : ControllerBase
{
private readonly ChannelWriter<WorkItem> _writer;
public ProcessController(Channel<WorkItem> ch)
=> _writer = channel.Writer;
[HttpPost]
public async Task<IActionResult> Enqueue(
WorkItem item, CancellationToken ct)
{
// Ждёт, если канал полный
await _writer.WriteAsync(item, ct);
return Accepted();
}
}
Если при перегрузке системы вы предпочитаете отклонять запросы (что правильнее публичного API, для предотвращения атак), используйте
TryWrite и возвращайте код 429:if (!_writer.TryWrite(item))
return StatusCode(
StatusCodes.Status429TooManyRequests,
"Сервис занят, попробуйте позже.");
return Accepted();
TryWrite никогда не блокирует выполнение. Он возвращает false сразу же, как только канал оказывается заполненным, что позволяет преобразовать эту ситуацию в чёткий ответ 429 вместо ожидания. Выбор стратегии зависит от того, кто инициирует вызов: если это внутренняя пакетная задача, можно позволить ей подождать, а если публичная конечная точка — лучше сразу отклонить запрос.
Потребитель: сервис, считывающий данные из канала
Потребитель реализован в виде BackgroundService, т.е. запускается и останавливается вместе с приложением. Весь цикл обработки умещается в одну строку благодаря методу ReadAllAsync, который выдаёт элементы до тех пор, пока канал не будет закрыт:
public class WorkConsumer : BackgroundService
{
private ChannelReader<WorkItem> _reader;
private ILogger<WorkConsumer> _logger;
public WorkConsumer(
Channel<WorkItem> channel,
ILogger<WorkConsumer> logger)
{
_reader = channel.Reader;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stopToken)
{
await foreach (var item in
_reader.ReadAllAsync(stopToken))
{
try
{
await ProcessAsync(item, stopToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Ошибка обработки {Id}", item.Id);
}
}
}
private async Task ProcessAsync(
WorkItem item, CancellationToken ct)
{
// … обработка элемента …
}
}
Замечания:
-
try/catch должен находиться внутри цикла и охватывать 1 элемент: если же он будет охватывать весь await foreach, то первое же исключение прервёт цикл, и потребитель завершит работу, в то время как приложение продолжит принимать новые задачи;-
ReadAllAsync корректно завершает выполнение, когда канал закрывается, что обеспечивает штатное завершение работы (об этом далее…).Окончание следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
👍7
День 2783. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Окончание
Начало
Продолжение
Настройка и запуск нескольких потребителей
Зарегистрируйте канал как синглтон, чтобы производитель и потребитель использовали один и тот же экземпляр, а затем добавьте столько экземпляров потребителя, сколько требуется для обеспечения нужной пропускной способности:
Количество потребителей — регулятор уровня параллелизма. Один потребитель обрабатывает задачи строго последовательно, а 3 — до трёх одновременно. Настраивайте их количество с учётом возможностей последующего этапа обработки: если
Корректное завершение работы
При остановке приложения в канале могут оставаться необработанные элементы. Если просто завершить процесс, они будут потеряны. Решение – закрыть канал записи при остановке и позволить потребителям обработать оставшиеся данные. За обработку сигнала отвечает небольшой фоновый сервис:
После вызова
Теперь при развёртывании новой версии системы очередь корректно опустошается, а не просто теряет все задачи, находившиеся в процессе обработки.
Когда использовать
Жизненный цикл каналов неразрывно связан с процессом приложения. Если приложение перезапускается, накопленные в буфере элементы исчезают: здесь нет ни записи на диск, ни механизма повторного воспроизведения, ни подтверждения доставки. Это вполне допустимо для задач, потерю которых можно себе позволить или которые можно выполнить заново: например, создание миниатюр изображений, предварительный прогрев кэша или отправка некритичных уведомлений. Однако такой подход неприемлем для обработки платежей или отправки email.
Если вам нужны гарантии сохранности данных при перезапусках, механизмы повторных попыток с обработкой «мертвых» сообщений или распределение задач между разными сервисами — значит, вы переросли возможности каналов. Тогда лучше использовать полноценный брокер сообщений или паттерн Outbox.
Важно осознавать ограничения системы до того, как вы выпустите её в прод, а не после того, как первая же перезагрузка приведет к потере всей очереди.
FAQ
1. В чём разница между ограниченным (bounded) и неограниченным (unbounded) каналом?
Неограниченный канал принимает данные для записи бесконечно, поэтому при быстром производителе и медленном потребителе очередь будет расти, пока не закончится память. Ограниченный канал имеет фиксированную ёмкость; когда он заполнен, операции записи приостанавливаются (или данные отбрасываются, в зависимости от режима обработки переполнения), пока потребитель не обработает накопленные элементы.
2. Чем Channel отличается от BlockingCollection?
BlockingCollection блокирует вызывающий поток, когда коллекция заполнена или пуста, тем самым занимая поток из пула на время ожидания. В канале методы WriteAsync и ReadAsync освобождают поток, поэтому ожидающий производитель или потребитель не потребляет ресурсы потока.
Итого
С появлением System.Threading.Channels реализация паттерна «производитель-потребитель» не требует сторонних библиотек. Ограниченный канал обеспечивает потокобезопасную асинхронную передачу данных с реальным механизмом обратного давления.
Критически важно:
- ограничить размер канала, чтобы всплеск нагрузки не привёл к падению процесса,
- корректно завершать работу писателя при выключении, чтобы накопленные данные обрабатывались, а не отбрасывались.
Если всё сделать правильно, то конечная точка, которая раньше «падала» под наплывом запросов на загрузку, продолжит стабильно работать.
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
Паттерн «Производитель-потребитель» c System.Threading.Channels. Окончание
Начало
Продолжение
Настройка и запуск нескольких потребителей
Зарегистрируйте канал как синглтон, чтобы производитель и потребитель использовали один и тот же экземпляр, а затем добавьте столько экземпляров потребителя, сколько требуется для обеспечения нужной пропускной способности:
builder.Services.AddSingleton(_ =>
Channel.CreateBounded<WorkItem>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
}));
// 3 потребителя одного канала
builder.Services.AddHostedService<WorkConsumer>();
builder.Services.AddHostedService<WorkConsumer>();
builder.Services.AddHostedService<WorkConsumer>();
Количество потребителей — регулятор уровня параллелизма. Один потребитель обрабатывает задачи строго последовательно, а 3 — до трёх одновременно. Настраивайте их количество с учётом возможностей последующего этапа обработки: если
ProcessAsync обращается к БД, поддерживающей не более 10 одновременных операций записи, не стоит запускать 50 потребителей.Корректное завершение работы
При остановке приложения в канале могут оставаться необработанные элементы. Если просто завершить процесс, они будут потеряны. Решение – закрыть канал записи при остановке и позволить потребителям обработать оставшиеся данные. За обработку сигнала отвечает небольшой фоновый сервис:
public class ChannelCompleter : IHostedService
{
private readonly ChannelWriter<WorkItem> _writer;
public ChannelCompleter(Channel<WorkItem> ch)
=> _writer = ch.Writer;
public Task StartAsync(CancellationToken ct)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken ct)
{
_writer.Complete();
return Task.CompletedTask;
}
}
После вызова
Complete() метод WriteAsync будет генерировать исключение при попытке записи новым производителем, а метод ReadAllAsync каждого потребителя продолжит выдавать элементы до тех пор, пока буфер не опустеет, после чего цикл завершится. Предоставьте хосту достаточно времени для завершения обработки всех данных, настроив параметр ShutdownTimeout:builder.Services.Configure<HostOptions>(o =>
o.ShutdownTimeout = TimeSpan.FromSeconds(30));
Теперь при развёртывании новой версии системы очередь корректно опустошается, а не просто теряет все задачи, находившиеся в процессе обработки.
Когда использовать
Жизненный цикл каналов неразрывно связан с процессом приложения. Если приложение перезапускается, накопленные в буфере элементы исчезают: здесь нет ни записи на диск, ни механизма повторного воспроизведения, ни подтверждения доставки. Это вполне допустимо для задач, потерю которых можно себе позволить или которые можно выполнить заново: например, создание миниатюр изображений, предварительный прогрев кэша или отправка некритичных уведомлений. Однако такой подход неприемлем для обработки платежей или отправки email.
Если вам нужны гарантии сохранности данных при перезапусках, механизмы повторных попыток с обработкой «мертвых» сообщений или распределение задач между разными сервисами — значит, вы переросли возможности каналов. Тогда лучше использовать полноценный брокер сообщений или паттерн Outbox.
Важно осознавать ограничения системы до того, как вы выпустите её в прод, а не после того, как первая же перезагрузка приведет к потере всей очереди.
FAQ
1. В чём разница между ограниченным (bounded) и неограниченным (unbounded) каналом?
Неограниченный канал принимает данные для записи бесконечно, поэтому при быстром производителе и медленном потребителе очередь будет расти, пока не закончится память. Ограниченный канал имеет фиксированную ёмкость; когда он заполнен, операции записи приостанавливаются (или данные отбрасываются, в зависимости от режима обработки переполнения), пока потребитель не обработает накопленные элементы.
2. Чем Channel отличается от BlockingCollection?
BlockingCollection блокирует вызывающий поток, когда коллекция заполнена или пуста, тем самым занимая поток из пула на время ожидания. В канале методы WriteAsync и ReadAsync освобождают поток, поэтому ожидающий производитель или потребитель не потребляет ресурсы потока.
Итого
С появлением System.Threading.Channels реализация паттерна «производитель-потребитель» не требует сторонних библиотек. Ограниченный канал обеспечивает потокобезопасную асинхронную передачу данных с реальным механизмом обратного давления.
Критически важно:
- ограничить размер канала, чтобы всплеск нагрузки не привёл к падению процесса,
- корректно завершать работу писателя при выключении, чтобы накопленные данные обрабатывались, а не отбрасывались.
Если всё сделать правильно, то конечная точка, которая раньше «падала» под наплывом запросов на загрузку, продолжит стабильно работать.
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet
👍5
День 2784. #Оффтоп
Утиная Типизация в C# с Помощью Перехватчиков
Если это ходит как утка и крякает как утка — значит, это утка. Применим утиную типизацию и заставим это работать в C# с помощью перехватчиков!
Примечание: этот пост написан в образовательных целях, но вы смело можете использовать описанный подход в проде 😉
В TypeScript можно сделать вот так:
Попробуем сделать что-то подобное в C#:
У классов A и B нет ничего общего: ни базового класса, ни общего интерфейса. Нужно придумать некую «форму» (обозначим её как
Кроме того, запрещено использовать следующие подходы:
-
- аргумент типа
- модификацию классов A или B (например, добавление общего базового класса или интерфейса).
Перехватчики
Начиная с .NET 8, в C# появилась экспериментальная функция компилятора — перехватчики. Генератор исходного кода может создать метод, пометить его атрибутом
Обычно это реализуется через сочетание пользовательских атрибутов (например,
Таким образом, с точки зрения использования нам всё равно потребуется нечто вроде интерфейса (или свойства) для «описания формы»:
Вы пишете
Полный код генератора/перехватчика на GitHub, а здесь рассмотрим наиболее интересные фрагменты.
Так или иначе, каждый вызов сводится к следующей обобщённой конструкции:
Вот и весь фокус. Это ведёт к незначительным накладным расходам (возможно инлайнинг поможет с этим). Таким образом, следующий код работает прекрасно:
Это также работает для свойств:
Это можно оформить в NuGet, если кто-то увидит в этом реальный смысл.
Итого
Полезно ли это? Вероятно, нет — но не в этом главная цель. Хотя язык не поддерживает это напрямую, подобное поведение можно в определённой мере «эмулировать».
Источник: https://steven-giesel.com/blogPost/5170e165-29e8-437a-b5ce-446a84943809/quack-quack-ducktyping-in-c-with-interceptors
Утиная Типизация в C# с Помощью Перехватчиков
Если это ходит как утка и крякает как утка — значит, это утка. Применим утиную типизацию и заставим это работать в C# с помощью перехватчиков!
Примечание: этот пост написан в образовательных целях, но вы смело можете использовать описанный подход в проде 😉
В TypeScript можно сделать вот так:
class A {
Do(): void { console.log("A.Do"); }
}
class B {
Do(): void { console.log("B.Do"); }
}
function foo(a: { Do(): void }) {
a.Do();
}Попробуем сделать что-то подобное в C#:
public class A
{
public void Do() => Console.WriteLine("A.Do");
}
public class B
{
public void Do() => Console.WriteLine("B.Do");
}
public void Foo(??? a) => a.Do();
У классов A и B нет ничего общего: ни базового класса, ни общего интерфейса. Нужно придумать некую «форму» (обозначим её как
???) — нечто такое, что позволит успешно скомпилировать вызовы Foo(new A()) и Foo(new B()) и обеспечит вызов нужного метода Do() в каждом случае, при этом вообще не затрагивая сами классы A и B.Кроме того, запрещено использовать следующие подходы:
-
dynamic (хотя это и невероятно крутая штука);- аргумент типа
object с последующим приведением типов через is или as;- модификацию классов A или B (например, добавление общего базового класса или интерфейса).
Перехватчики
Начиная с .NET 8, в C# появилась экспериментальная функция компилятора — перехватчики. Генератор исходного кода может создать метод, пометить его атрибутом
[InterceptsLocation], указав конкретное место вызова в вашем коде, и компилятор незаметно перенаправит вызов на этот метод. Это происходит без каких-либо затрат ресурсов во время выполнения, так как всё разрешается на этапе компиляции.Обычно это реализуется через сочетание пользовательских атрибутов (например,
DuckType и DuckShape), создаваемых генератором кода, и логики перехватчика: система находит все места вызовов и выполняет необходимые действия.Таким образом, с точки зрения использования нам всё равно потребуется нечто вроде интерфейса (или свойства) для «описания формы»:
[DuckShape]
public interface IDoable { void Do(); }
public static partial class Ops
{
[DuckTyped]
public static void Foo(IDoable a) => a.Do();
}
Вы пишете
Foo(IDoable a) => a.Do() - именно так, как вам хочется. Атрибут [DuckShape] помечает IDoable как структурный контракт, а не как интерфейс, который нужно реализовывать вручную. Атрибут [DuckTyped] сообщает генератору: «Вот настоящая логика; сделай так, чтобы её можно было вызывать с любым объектом, подходящим по структуре».Полный код генератора/перехватчика на GitHub, а здесь рассмотрим наиболее интересные фрагменты.
Так или иначе, каждый вызов сводится к следующей обобщённой конструкции:
[InterceptsLocation(1, "…")]
public static void Interceptor_1(A value)
{
Ops.Foo((IDoable)(new ShapeAdapter_IDoable_A(value)));
}
ShapeAdapter_IDoable_A - это небольшая генерируемая структура только для чтения, которая реализует IDoable, перенаправляя сразу в A:internal readonly struct
ShapeAdapter_IDoable_A : IDoable
{
private readonly A _value;
public ShapeAdapter_IDoable_A(A value)
=> _value = value;
public void Do() => _value.Do();
}
Вот и весь фокус. Это ведёт к незначительным накладным расходам (возможно инлайнинг поможет с этим). Таким образом, следующий код работает прекрасно:
Ops.Foo(new A()); // "A.Do"
Ops.Foo(new B()); // "B.Do"
Это также работает для свойств:
[DuckShape]
public interface INameable
{ string Name { get; set; } }
public class Person
{ public string Name { get; set; } = ""; }
[DuckTyped]
public static string Greet(INameable n)
=> $"Hello, {n.Name}!";
Greet(new Person { Name = "Jon" }); // "Hello, Jon!"
Это можно оформить в NuGet, если кто-то увидит в этом реальный смысл.
Итого
Полезно ли это? Вероятно, нет — но не в этом главная цель. Хотя язык не поддерживает это напрямую, подобное поведение можно в определённой мере «эмулировать».
Источник: https://steven-giesel.com/blogPost/5170e165-29e8-437a-b5ce-446a84943809/quack-quack-ducktyping-in-c-with-interceptors
👍7
День 2785. #ЗаметкиНаПолях
Обеспечиваем Изоляцию Тенантов с Помощью PostgreSQL
Механизм защиты на уровне строк (Row-Level Security, RLS) в PostgreSQL добавляет проверку на стороне БД поверх фильтров запросов EF Core. Он контролирует операции чтения и записи при условии, что приложение подключается под ролью, не являющейся владельцем таблицы, и устанавливает идентификатор тенанта при каждом открытии соединения.
Популярна рекомендация использовать глобальные фильтры запросов для реализации мультитенантности. EF Core добавляет условие
Требуется дополнительный уровень проверки, не зависящий от того, помнит ли каждый разработчик о первом механизме. В PostgreSQL такая возможность уже встроена благодаря RLS.
Настройка правила в PostgreSql
Защита на уровне строк (RLS) представляет собой предикат, который PostgreSQL автоматически добавляет к каждой команде, выполняемой для таблицы (за исключением ролей, имеющих привилегию обхода RLS). Приложение не может «забыть» об этом условии, т.к. оно даже не видит его.
Суперпользователи, роли с атрибутом
Условие
Функция
Установка идентификатора тенанта для каждого соединения
Политика использует переменную сессии, поэтому приложение должно её устанавливать; логичнее всего делать это в начале обработки запроса. Однако такой подход быстро приводит к проблемам.
EF Core открывает соединение для выполнения команды и закрывает его по завершении, а Npgsql сбрасывает состояние сессии в пуле; в результате запрос, следующий за командой
Решение в том, чтобы устанавливать идентификатор тенанта при каждом открытии соединения — независимо от того, какое именно физическое соединение предоставляет Npgsql. Эту задачу выполняет перехватчик соединений (connection interceptor), использующий
Метод set_config принимает tenant в качестве параметра привязки, а значение false указывает на область видимости - сессия. Конструкция
Регистрируем сервисы с соответствующей областью видимости в Program.cs:
Здесь используется
Это влечет за собой дополнительные затраты в виде лишнего сетевого запроса при каждом открытии соединения — примерно 0,4 мс на запрос.
Попытка обойти ограничение
Отключим фильтр:
Метод
Аналогично при работе с присоединённой (attached) сущностью. EF Core формирует запрос вида
Чтение без фильтрации по-прежнему возвращает только счета текущего тенанта, а попытка вставки записи с идентификатором другого тенанта завершается ошибкой с кодом SQLSTATE 42501.
Чего политика не может сделать, так это определить, какой тенант является «правильным». Если приложение ошибочно определит тенанта, политика применит ограничения именно для этого (неверного) тенанта.
Итого
Для многотенантных приложений рекомендуется использовать фильтр запросов EF в сочетании с политикой RLS. Фильтр включает идентификатор тенанта в SQL-запрос, благодаря чему планы выполнения остаются простыми, намерение разработчика ясно видно в коде, а накладные расходы во время выполнения отсутствуют. Политика же обеспечивает защиту даже в тех случаях, когда стандартный фильтр не задействован — например, при использовании
Не забудьте предусмотреть роль, позволяющую обходить политику безопасности; это необходимо для выполнения резервного копирования и фоновых задач, затрагивающих данные разных тенантов.
Источник: https://milanjovanovic.tech/blog/postgres-row-level-security-with-ef-core
Обеспечиваем Изоляцию Тенантов с Помощью PostgreSQL
Механизм защиты на уровне строк (Row-Level Security, RLS) в PostgreSQL добавляет проверку на стороне БД поверх фильтров запросов EF Core. Он контролирует операции чтения и записи при условии, что приложение подключается под ролью, не являющейся владельцем таблицы, и устанавливает идентификатор тенанта при каждом открытии соединения.
Популярна рекомендация использовать глобальные фильтры запросов для реализации мультитенантности. EF Core добавляет условие
WHERE tenant_id = @tenant к каждому генерируемому запросу, поэтому разработчикам не нужно помнить об этом предикате. Однако действие фильтра ограничено: SQL-команды, отправляемые через ExecuteSql, присоединённые сущности, а также запросы с вызовом IgnoreQueryFilters() остаются вне зоны его влияния.Требуется дополнительный уровень проверки, не зависящий от того, помнит ли каждый разработчик о первом механизме. В PostgreSQL такая возможность уже встроена благодаря RLS.
Настройка правила в PostgreSql
Защита на уровне строк (RLS) представляет собой предикат, который PostgreSQL автоматически добавляет к каждой команде, выполняемой для таблицы (за исключением ролей, имеющих привилегию обхода RLS). Приложение не может «забыть» об этом условии, т.к. оно даже не видит его.
Суперпользователи, роли с атрибутом
BYPASSRLS и владельцы таблиц обходят это ограничение. Во многих приложениях один и тот же пользователь БД выполняет миграции, обрабатывает запросы и является владельцем таблиц. Поэтому в этом примере приложение подключается под ролью app_user, которая не владеет объектами и имеет лишь необходимые права DML, тогда как миграции выполняются от имени отдельной роли-владельца. Такое разделение также предотвращает возможность выполнения команды DROP POLICY со стороны приложения. Вот пример политики для таблицы счетов (invoices):ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);
Условие
USING определяет, к каким строкам возможен доступ при чтении, обновлении или удалении, а WITH CHECK отклоняет операции вставки или обновления, если новый tenant_id не совпадает с параметром сессии. Использование NULLIF гарантирует, что отсутствующий или пустой параметр не будет соответствовать ничему — именно такой вариант сбоя предпочтителен в данном случае. Опция FORCE распространяет действие политики и на владельца таблицы, предотвращая случайное изменение строк всех тенантов скриптами миграции.Функция
current_setting возвращает текущее значение параметра конфигурации.Установка идентификатора тенанта для каждого соединения
Политика использует переменную сессии, поэтому приложение должно её устанавливать; логичнее всего делать это в начале обработки запроса. Однако такой подход быстро приводит к проблемам.
EF Core открывает соединение для выполнения команды и закрывает его по завершении, а Npgsql сбрасывает состояние сессии в пуле; в результате запрос, следующий за командой
SET, выполняется в соединении, «не знающем» об этом тенанте. Отключение сброса с помощью параметра No Reset On Close=true делает ситуацию ещё хуже: следующий запрос «наследует» настройки предыдущего тенанта и получает доступ к строкам, которые не должен видеть.Решение в том, чтобы устанавливать идентификатор тенанта при каждом открытии соединения — независимо от того, какое именно физическое соединение предоставляет Npgsql. Эту задачу выполняет перехватчик соединений (connection interceptor), использующий
TenantContext для хранения идентификатора тенанта, авторизованного для текущего запроса:public class TenantConnectionInterceptor(
TenantContext tenant)
: DbConnectionInterceptor
{
public override void ConnectionOpened(
DbConnection conn,
ConnectionEndEventData eventData)
{
using var cmd = Build(conn);
cmd.ExecuteNonQuery();
}
public override async Task ConnectionOpenedAsync(
DbConnection conn,
ConnectionEndEventData eventData,
CancellationToken ct = default)
{
await using var cmd = Build(conn);
await cmd.ExecuteNonQueryAsync(ct);
}
private DbCommand Build(DbConnection conn)
{
var cmd = conn.CreateCommand();
cmd.CommandText =
"SELECT set_config('app.tenant_id', @tenant, false)";
var prm = cmd.CreateParameter();
prm.ParameterName = "tenant";
prm.Value = tenant.TenantId?.ToString() ?? "";
cmd.Parameters.Add(prm);
return cmd;
}
}
Метод set_config принимает tenant в качестве параметра привязки, а значение false указывает на область видимости - сессия. Конструкция
?? "" обеспечивает блокировку запроса в случае отсутствия tenant.Регистрируем сервисы с соответствующей областью видимости в Program.cs:
builder.Services.AddScoped<TenantContext>();
builder.Services.AddScoped<TenantConnectionInterceptor>();
builder.Services.AddDbContext<AppDbContext>(
(sp, opts) => opts
.UseNpgsql(connString)
.AddInterceptors(
sp.GetRequiredService<TenantConnectionInterceptor>())
);
Здесь используется
AddDbContext, а не AddDbContextPool, т.к. контекст из пула сохранял бы TenantContext от первого запроса. При работе через PgBouncer в режиме пулинга транзакций идентификатор арендатора (tenant) необходимо задавать с помощью set_config(…, true) внутри явной транзакции.Это влечет за собой дополнительные затраты в виде лишнего сетевого запроса при каждом открытии соединения — примерно 0,4 мс на запрос.
Попытка обойти ограничение
Отключим фильтр:
var affected = await db.Invoices
.IgnoreQueryFilters()
.Where(i => i.Id == otherTenantId)
.ExecuteUpdateAsync(
s => s.SetProperty(i => i.Status, "Cancelled"),
cancellationToken);
// affected == 0
Метод
ExecuteUpdateAsync отправляет SQL-запрос немедленно, минуя механизмы отслеживания изменений и SaveChanges, поэтому фильтр не применяется. Поскольку строка принадлежит другому тенанту, политика скрывает её, и операция обновления не затрагивает ни одной записи.Аналогично при работе с присоединённой (attached) сущностью. EF Core формирует запрос вида
UPDATE invoices SET … WHERE id = @p0 на основе первичного ключа, политика добавляет свой предикат, и в итоге ни одна строка не удовлетворяет условиям. EF сообщает об этом как об исключении DbUpdateConcurrencyException (ошибка параллелизма), что выглядит как проигрыш в состоянии гонки при оптимистической блокировке, хотя данная строка изначально не была видна в текущей сессии.Чтение без фильтрации по-прежнему возвращает только счета текущего тенанта, а попытка вставки записи с идентификатором другого тенанта завершается ошибкой с кодом SQLSTATE 42501.
Чего политика не может сделать, так это определить, какой тенант является «правильным». Если приложение ошибочно определит тенанта, политика применит ограничения именно для этого (неверного) тенанта.
Итого
Для многотенантных приложений рекомендуется использовать фильтр запросов EF в сочетании с политикой RLS. Фильтр включает идентификатор тенанта в SQL-запрос, благодаря чему планы выполнения остаются простыми, намерение разработчика ясно видно в коде, а накладные расходы во время выполнения отсутствуют. Политика же обеспечивает защиту даже в тех случаях, когда стандартный фильтр не задействован — например, при использовании
IgnoreQueryFilters() для формирования административного отчёта.Не забудьте предусмотреть роль, позволяющую обходить политику безопасности; это необходимо для выполнения резервного копирования и фоновых задач, затрагивающих данные разных тенантов.
Источник: https://milanjovanovic.tech/blog/postgres-row-level-security-with-ef-core
👍7
Какой диапазон выдаст метод TimeSpan.Parse("24:00:00")?
Anonymous Quiz
69%
24 часа
13%
24 дня
7%
24 минуты
1%
24 секунды
9%
другой
День 2786. #AI
Пусть Copilot Поспорит с Вами
Как разработчик или архитектор, вы ежедневно принимаете множество проектных решений. Вы выбираете Redis для кэширования, отдаете предпочтение REST вместо GraphQL и так далее. Приняв решение, вы движетесь дальше, но где-то на заднем плане звучит внутренний голос: «Стоит ли беспокоить коллегу и спрашивать его мнение?», «А вдруг я что-то упустил?», «Действительно ли это правильный выбор?» GitHub Copilot может помочь вам в этом с помощью новой команды: /spar.
Что это?
Команда
Примечание: это отличается от команды
Как пользоваться?
Введите
Проверка архитектурного решения:
Сравнение вариантов реализации:
Анализ плана миграции:
Или проверка оптимизации производительности перед выпуском:
Совет: чем конкретнее ваш запрос, тем более глубокой и точной будет критика. Фраза «Подвергни критике мою стратегию кэширования» приведёт лишь к общим замечаниям. Если же вы назовете конкретный аспект, вызывающий сомнения (например, стратегия инвалидации кэша, согласованность данных или масштабируемость), то получите по-настоящему содержательный диалог.
Место в рабочем процессе
Команда
Чем отличается от /grill-me?
Если вы следите за экосистемой навыков, то команда
-
-
Ещё несколько отличий:
- Роль:
- Входные данные:
- Состояние:
- Риски: в случае со
Если попытаться выстроить их на одной шкале:
Конечно, такой подход не выявит абсолютно все проблемы. В конце концов, это всё тот же Copilot, который спорит сам с собой через вашу клавиатуру. И всё же, когда система сама озвучивает вопрос «А подумали ли мы, что произойдет в случае сбоя?» — ещё до того, как об этом спросит коллега, — это простой и полезный прием.
Источник: https://bartwullems.blogspot.com/2026/09/let-copilot-argue-with-you-spar-slash.html
Пусть Copilot Поспорит с Вами
Как разработчик или архитектор, вы ежедневно принимаете множество проектных решений. Вы выбираете Redis для кэширования, отдаете предпочтение REST вместо GraphQL и так далее. Приняв решение, вы движетесь дальше, но где-то на заднем плане звучит внутренний голос: «Стоит ли беспокоить коллегу и спрашивать его мнение?», «А вдруг я что-то упустил?», «Действительно ли это правильный выбор?» GitHub Copilot может помочь вам в этом с помощью новой команды: /spar.
Что это?
Команда
/spar переключает Copilot из режима «помоги мне это создать» в режим «убеди меня, что это плохая идея». Вместо того чтобы просто принять ваш план и сгенерировать код, Copilot начинает ставить под сомнение ваши допущения, спрашивает о граничных случаях и указывает на компромиссы, которые вы могли не заметить.Примечание: это отличается от команды
/plan, которая помогает разбить задачу на части. /spar исходит из того, что план уже есть, и стремится подвергнуть его стресс-тестированию, прежде чем вы приступите к реализации.Как пользоваться?
Введите
/spar в поле чата, а затем укажите, какое решение хотите подвергнуть критике. Вот несколько примеров из документации:Проверка архитектурного решения:
/spar Я планирую использовать Redis в качестве уровня кэширования для API продукта. Проверь мой подход и укажи на возможные проблемы с масштабируемостью или согласованностью данных, которые я мог упустить.
Сравнение вариантов реализации:
/spar Помоги мне выбрать между REST и GraphQL для клиентского API. Задавай вопросы, ставь под сомнение мои допущения и порекомендуй подход, который лучше всего подойдет для приложения с мобильными клиентами.
Анализ плана миграции:
/spar Я переношу нашу базу данных на новый управляемый сервис с минимальным временем простоя. Найди слабые места в моём плане миграции и укажи на риски или граничные случаи, которые стоит учесть.
Или проверка оптимизации производительности перед выпуском:
/spar Я планирую использовать ленивую загрузку (lazy loading) для большинства компонентов сайта, чтобы сократить время начальной загрузки. Оцени мой подход и скажи, где он может ухудшить пользовательский опыт или создать излишнюю сложность.
Совет: чем конкретнее ваш запрос, тем более глубокой и точной будет критика. Фраза «Подвергни критике мою стратегию кэширования» приведёт лишь к общим замечаниям. Если же вы назовете конкретный аспект, вызывающий сомнения (например, стратегия инвалидации кэша, согласованность данных или масштабируемость), то получите по-настоящему содержательный диалог.
Место в рабочем процессе
Команда
/spar работает внутри приложения GitHub Copilot (а не в CLI или чате VS Code); это одна из специализированных команд, ориентированных на многосеансовый рабочий процесс, наряду с /plan, /autopilot, /rubber-duck и /orchestrate. Разумный сценарий использования выглядит так: /plan — для разбивки задачи, /spar — для проверки плана на прочность (до внесения изменений в код), а затем /autopilot — для его реализации.Чем отличается от /grill-me?
Если вы следите за экосистемой навыков, то команда
/grill-me от Мэтта Покока может показаться аналогичной. Но это не так, и важно понимать разницу:-
/spar исходит из того, что у вас уже есть решение или подход, и пытается их опровергнуть. Вы предлагаете что-то конкретное — например, «я использую Redis для кэширования», — а команда выступает оппонентом, критикуя ваш план.-
/grill-me предполагает, что решения у вас ещё нет. Команда берёт за основу общую идею и проводит серию опросов, пока вы не определитесь с конкретным вариантом. Здесь нет готового плана, который нужно критиковать; система пытается «вытянуть» его из вас, задавая вопросы последовательно — по одной теме за раз, — чтобы не спрашивать о том, что зависит от ещё не полученного ответа.Ещё несколько отличий:
- Роль:
/spar — критик. /grill-me — интервьюер.- Входные данные:
/spar требует сформулированного подхода для критики. /grill-me достаточно лишь смутного направления. Точность и конкретика — это результат, а не исходные данные.- Состояние:
/grill-me работает без сохранения состояния. Никаких файлов, рабочих областей или следов — только более чёткая идея у вас в голове. /spar выполняется в рамках сессии или рабочей области приложения Copilot, привязанных к контексту текущего проекта.- Риски: в случае со
/spar есть риск проигнорировать критику и продолжить работу без изменений. Для /grill-me задокументированный риск — это пассивность: отвечать «согласен, согласен, согласен» на сорок вопросов и получить план, который составил агент, а вы лишь кивали в знак согласия. Примечание: возможно, более интересная параллель — это не /spar против /grill-me, а /spar против /rubber-duck. В режиме /rubber-duck тоже используется вторая модель для независимого анализа уже проделанной работы; по своей концепции это ближе к /grill-me, чем к /spar. /spar спорит с вами в режиме реального времени в ходе того же диалога, тогда как /rubber-duck и интервью в стиле /grill-me предполагают более независимый и структурированный этап дополнительной проверки.Если попытаться выстроить их на одной шкале:
/grill-me работает на этапе до появления плана, /plan помогает этот план составить, /spar подвергает его критике, когда он уже готов, а /rubber-duck позволяет взглянуть на него со стороны перед финальным выпуском.Конечно, такой подход не выявит абсолютно все проблемы. В конце концов, это всё тот же Copilot, который спорит сам с собой через вашу клавиатуру. И всё же, когда система сама озвучивает вопрос «А подумали ли мы, что произойдет в случае сбоя?» — ещё до того, как об этом спросит коллега, — это простой и полезный прием.
Источник: https://bartwullems.blogspot.com/2026/09/let-copilot-argue-with-you-spar-slash.html
👍7
День 2787. #ЗаметкиНаПолях #Middleware
Что Такое Промежуточное ПО и Его Подводные Камни. Начало
В этой серии постов разберём, что представляет собой промежуточное ПО, как написать собственный компонент и с какими неожиданными проблемами могут столкнуться разработчики.
Создание промежуточного ПО
Промежуточное ПО добавляется в файле Program.cs с помощью метода
Промежуточное ПО выполняется при каждом HTTP-запросе. Каждый запрос проходит через него на входе, а ответ — на выходе.
Вызов
Недостаток подхода с
Не забывайте внедрять
Затем зарегистрируйте этот компонент в файле Program.cs; вызов
Если не зарегистрировать класс, промежуточное ПО никогда не выполнится.
Порядок регистрации промежуточного ПО в Program.cs имеет значение. Если мы зарегистрируем RequestLoggingMiddleware первым, этот компонент первым обработает запрос, а ответ – последним:
Если забыть вызвать next()
При запуске приложения страница не отобразится. При вызове конечной точки API через Postman вместо строки «Это тест» возвращается пустой ответ со статусом 200.
Это происходит из-за того, что
Промежуточное ПО — это синглтон, а InvokeAsync имеет время жизни scoped
Зарегистрируйте scoped-сервис в Program.cs:
Теперь внедрите его в RequestLoggingMiddleware.
При запуске приложения возникает ошибка: Cannot resolve scoped service 'IMyScopedService' from root provider (Не удаётся разрешить scoped-сервис IMyScopedService из корневого провайдера).
Это происходит потому, что метод UseMiddleware создаёт экземпляр класса промежуточного ПО один раз, используя корневой провайдер сервисов приложения, и повторно использует этот же экземпляр для каждого запроса. Сервис с областью видимости scoped нельзя получить напрямую из корневого провайдера.
Решение в том, чтобы убрать IMyScopedService из конструктора и внедрять его непосредственно в метод InvokeAsync:
Метод InvokeAsync выполняется заново для каждого запроса, поэтому ASP.NET Core каждый раз разрешает IMyScopedService из области видимости текущего запроса.
Окончание следует…
Источник: https://www.roundthecode.com/dotnet-tutorials/what-is-middleware-aspnet-core-and-gotchas
Что Такое Промежуточное ПО и Его Подводные Камни. Начало
В этой серии постов разберём, что представляет собой промежуточное ПО, как написать собственный компонент и с какими неожиданными проблемами могут столкнуться разработчики.
Создание промежуточного ПО
Промежуточное ПО добавляется в файле Program.cs с помощью метода
app.Use():app.Use(async (ctx, next) =>
{
Console.WriteLine($"Запрос: {ctx.Request.Path}");
await next(ctx);
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
});
Промежуточное ПО выполняется при каждом HTTP-запросе. Каждый запрос проходит через него на входе, а ответ — на выходе.
Вызов
next() играет ключевую роль: он обеспечивает переход к следующему компоненту в конвейере обработки. Если забыть его добавить, выполнение конвейера остановится, и все последующие компоненты промежуточного ПО не будут запущены.Недостаток подхода с
app.Use() в использовании лямбда-выражения, что затрудняет тестирование. Более удачное решение — вынести логику в отдельный класс:public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next) =>
_next = next;
public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Запрос: {ctx.Request.Path}");
await _next(ctx);
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
}
}
Не забывайте внедрять
next в конструктор, чтобы использовать его для вызова следующего компонента промежуточного ПО в конвейере.Затем зарегистрируйте этот компонент в файле Program.cs; вызов
app.Use(…) можно удалить:app.UseMiddleware<RequestLoggingMiddleware>();
Если не зарегистрировать класс, промежуточное ПО никогда не выполнится.
Порядок регистрации промежуточного ПО в Program.cs имеет значение. Если мы зарегистрируем RequestLoggingMiddleware первым, этот компонент первым обработает запрос, а ответ – последним:
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseSwaggerUI(opts =>
{
opts.SwaggerEndpoint("/openapi/v1.json", "Api");
});
app.MapOpenApi();
var group = app.MapGroup("/api");
group.MapGet("test", () =>
TypedResults.Content("Это тест", "text/plain"););
Если забыть вызвать next()
public class RequestLoggingMiddleware
{
…
public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Запрос: {ctx.Request.Path}");
//await _next(ctx);
Console.WriteLine($"Response: {ctx.Response.StatusCode}");
}
}
При запуске приложения страница не отобразится. При вызове конечной точки API через Postman вместо строки «Это тест» возвращается пустой ответ со статусом 200.
Это происходит из-за того, что
RequestLoggingMiddleware зарегистрирован первым. Если он прерывает цепочку и не вызывает next(), то ни один из последующих компонентов промежуточного ПО, включая эндпоинты API и Swagger, не выполнится. Это полезно в некоторых случаях, когда вы хотите прервать обработку в зависимости от некоторого условия. Однако чаще всего необходимо вызывать next().Промежуточное ПО — это синглтон, а InvokeAsync имеет время жизни scoped
Зарегистрируйте scoped-сервис в Program.cs:
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
Теперь внедрите его в RequestLoggingMiddleware.
public class RequestLoggingMiddleware
{
//…
public RequestLoggingMiddleware(
RequestDelegate next,
IMyScopedService mySvc)
{
//…
}
//…
}
При запуске приложения возникает ошибка: Cannot resolve scoped service 'IMyScopedService' from root provider (Не удаётся разрешить scoped-сервис IMyScopedService из корневого провайдера).
Это происходит потому, что метод UseMiddleware создаёт экземпляр класса промежуточного ПО один раз, используя корневой провайдер сервисов приложения, и повторно использует этот же экземпляр для каждого запроса. Сервис с областью видимости scoped нельзя получить напрямую из корневого провайдера.
Решение в том, чтобы убрать IMyScopedService из конструктора и внедрять его непосредственно в метод InvokeAsync:
public class RequestLoggingMiddleware
{
//…
public async Task InvokeAsync(
HttpContext context,
IMyScopedService mySvc)
{
//…
}
}
Метод InvokeAsync выполняется заново для каждого запроса, поэтому ASP.NET Core каждый раз разрешает IMyScopedService из области видимости текущего запроса.
Окончание следует…
Источник: https://www.roundthecode.com/dotnet-tutorials/what-is-middleware-aspnet-core-and-gotchas
👍7
День 2788. #ЗаметкиНаПолях #Middleware
Что Такое Промежуточное ПО и Его Подводные Камни. Окончание
Начало
Изменение ответа после начала его отправки
В данном случае промежуточное ПО обновляет код ответа после возврата управления из вызова
Это приводит к возникновению исключения: System.InvalidOperationException: StatusCode cannot be set because the response has already started. (StatusCode нельзя установить, так как отправка ответа уже началась)
После того как отправка ответа началась, изменить код состояния или снова записать данные в ответ уже невозможно. Однако можно выполнить необходимые действия непосредственно перед началом отправки, воспользовавшись событием
В методе
Удержание scoped-сервисов после завершения обработки запроса
Добавим делегат OnCompleted в промежуточное ПО и создадим новую фоновую задачу, которая будет выполнена с задержкой в 5 секунд:
Код выполняется, и на первый взгляд всё в порядке. Страница отображается корректно, а в логах видно, что метод
Проблема в том, что после завершения формирования ответа scoped-сервис уже оказывается утилизированным (disposed), поскольку он был внедрён в метод
Страница по-прежнему отображается корректно, но в логах теперь исключение: Cannot access a disposed object (Попытка доступа к объекту, который уже был утилизирован).
Более надёжный подход — создать новую область видимости для подобных фоновых задач, а не повторно использовать экземпляр, привязанный к области видимости текущего запроса:
Благодаря внедрению
Источник: https://www.roundthecode.com/dotnet-tutorials/what-is-middleware-aspnet-core-and-gotchas
Что Такое Промежуточное ПО и Его Подводные Камни. Окончание
Начало
Изменение ответа после начала его отправки
В данном случае промежуточное ПО обновляет код ответа после возврата управления из вызова
next():public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Запрос: {ctx.Request.Path}");
await _next(ctx);
// Изменение статуса и ответа
ctx.Response.StatusCode =
StatusCodes.Status503ServiceUnavailable;
await ctx.Response.WriteAsync(
"<p>Эта страница недоступна</p>");
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
}
Это приводит к возникновению исключения: System.InvalidOperationException: StatusCode cannot be set because the response has already started. (StatusCode нельзя установить, так как отправка ответа уже началась)
После того как отправка ответа началась, изменить код состояния или снова записать данные в ответ уже невозможно. Однако можно выполнить необходимые действия непосредственно перед началом отправки, воспользовавшись событием
context.Response.OnStarting:public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Request: {ctx.Request.Path}");
ctx.Response.OnStarting(async() =>
{
if (ctx.Response.HasStarted)
return;
// Изменение статуса и ответа
ctx.Response.StatusCode =
StatusCodes.Status503ServiceUnavailable;
await ctx.Response.WriteAsync(
"<p>Эта страница недоступна</p>");
});
await _next(context);
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
}
В методе
OnStarting сначала проверяется свойство HasStarted, и, если оно уже имеет значение true, выполнение метода досрочно завершается. В противном случае в ответ выводится сообщение о том, что страница в данный момент недоступна.Удержание scoped-сервисов после завершения обработки запроса
public class MyScopedService
: IMyScopedService, IDisposable
{
public bool Disposed { get; private set; }
public void Test()
{
_logger.LogInformation(
"MyScopedService.Test() успешно выполнено");
}
public void Dispose()
{
Disposed = true;
}
}
Добавим делегат OnCompleted в промежуточное ПО и создадим новую фоновую задачу, которая будет выполнена с задержкой в 5 секунд:
public class RequestLoggingMiddleware
{
//…
public async Task InvokeAsync(
HttpContext ctx,
IMyScopedService mySvc)
{
_logger.LogInformation($"Запрос: {ctx.Request.Path}");
//…
ctx.Response.OnCompleted(() =>
{
var _ = Task.Run(async() =>
{
await Task.Delay(5000);
try
{
mySvc.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
});
return Task.CompletedTask;
});
//…
}
}
Код выполняется, и на первый взгляд всё в порядке. Страница отображается корректно, а в логах видно, что метод
MyScopedService.Test() срабатывает при каждом запросе. Однако здесь кроется скрытая проблема, зависящая от того, как именно вы используете этот сервис.Проблема в том, что после завершения формирования ответа scoped-сервис уже оказывается утилизированным (disposed), поскольку он был внедрён в метод
InvokeAsync. Чтобы продемонстрировать это, добавьте в MyScopedService проверку, которая будет выбрасывать исключение, если сервис уже утилизирован.public class MyScopedService
: IMyScopedService, IDisposable
{
//…
public void Test()
{
ObjectDisposedException.ThrowIf(Disposed, this);
_logger.LogInformation(
"MyScopedService.Test() успешно выполнено");
}
//…
}
Страница по-прежнему отображается корректно, но в логах теперь исключение: Cannot access a disposed object (Попытка доступа к объекту, который уже был утилизирован).
Более надёжный подход — создать новую область видимости для подобных фоновых задач, а не повторно использовать экземпляр, привязанный к области видимости текущего запроса:
public class RequestLoggingMiddleware
{
//…
public async Task InvokeAsync(
HttpContext ctx,
IMyScopedService mySvc,
IServiceScopeFactory scopeFactory)
{
//…
ctx.Response.OnCompleted(() =>
{
var _ = Task.Run(async() =>
{
// Создаём новую область видимости
using var scope =
_scopeFactory.CreateScope();
mySvc = scope.ServiceProvider
.GetRequiredService<IMyScopedService>();
await Task.Delay(5000);
try
{
mySvc.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
_logger.LogInformation("Task completed");
});
return Task.CompletedTask;
});
await _next(ctx);
//…
}
}
Благодаря внедрению
IServiceScopeFactory в промежуточное ПО и созданию новой области видимости (scope) внутри фоновой задачи, mySvc теперь разрешается в рамках этой новой области, а не той, что привязана к исходному запросу; следовательно, сервис не уничтожается к моменту выполнения отложенной операции.Источник: https://www.roundthecode.com/dotnet-tutorials/what-is-middleware-aspnet-core-and-gotchas
👍3
День 2789. #Оффтоп #AI
Важно ли По-прежнему Качество Кода?
Автор оригинала: Марк Симан
Пока мы используем LLM как инструмент для генерации кода, за который в итоге отвечают люди, качество кода остается важным. Людям приходится проверять этот код, работать с ним, исправлять ошибки и т.п. Но что произойдёт, если люди перестанут участвовать в этом процессе? Если в будущем весь код будут писать LLM, будет ли иметь значение качество кода?
Почему качество кода имеет значение
Выражение «писать код в стиле YOLO (You Only Look Once)» означает создание кода без оглядки на его качество — практика, которая, откровенно говоря, преобладала десятилетиями. И всё же компании-разработчики, позволяющие своим сотрудникам так работать, в итоге сталкиваются с проблемами. Они накапливают столько технического долга, что больше не могут своевременно реагировать на запросы бизнеса.
Важно уточнить: качество кода — это не то же самое, что качество ПО. Качество кода — это внутреннее свойство кодовой базы: то, как код структурирован, насколько он читаем и как легко его изменять.
До сих пор все это сводилось к вопросам человеческого восприятия и мышления. Именно этому посвящена книга «Код, который умещается в голове». Код писали люди. Людям приходилось его редактировать. А что, если ситуация изменится?
Языки программирования для LLM
Представим будущее, в котором люди больше не пишут и не читают код. Важно ли в таком случае качество кода? Очевидно, что если люди перестанут работать с кодом, то все ограничения, связанные с особенностями человеческого мышления, потеряют актуальность. Полученный код может отличаться чрезмерно длинными методами, высокой цикломатической сложностью, невнятными именами переменных и сильной связностью компонентов. Более того, некоторые из этих понятий могут вообще утратить смысл. Само понятие «длинный метод» предполагает, что ПО вообще разбито на методы. Спойлер: в машинном коде их нет.
В реальности я не ожидаю, что LLM будут генерировать машинный код напрямую — хотя бы потому, что машинный код не обладает переносимостью. С другой стороны, нет оснований полагать, что в этом гипотетическом будущем LLM будут придерживаться языков, существующих сегодня. За исключением машинного кода, все языки программирования созданы для того, чтобы облегчить процесс написания кода людьми. Если люди перестанут смотреть на код, вполне вероятно появление новых языков, оптимизированных для того, чтобы LLM могли их воспринимать, генерировать и преобразовывать. Предположим, что появится один такой язык. Назовём его LLaMe.
Техдолг для LLM
Будет ли у LLM возникать техдолг? Непонятно. Возможно, нет. Тогда всё описанное далее теряет смысл: мы просто позволим LLM бесконечно генерировать код на LLaMe, не заботясь о последствиях, и это никогда не станет проблемой.
И все же я допускаю, что техдолг может накапливаться. Даже при работе с машинным кодом существуют варианты структурирования: например, можно повторно использовать регистр или задействовать два разных регистра для выполнения операции.
Следовательно, нужно исходить из того, что LLaMe будет допускать различные варианты структурирования кода. Некоторые из этих структур могут сложнее поддаваться изменениям, чем другие. Для «понимания» некоторых структур LLM может потребоваться больше ресурсов (токенов?). Хорошо структурированный код на LLaMe позволит LLM быстро и с минимальными затратами добавлять новые функции и исправлять ошибки. Улучшение же плохо структурированного кода на LLaMe потребует больше времени и ресурсов.
Могут ли LLM предотвратить появление техдолга?
Как выглядит плохой код, написанный LLM (назовем этот стиль LLaMe)? Мы даже не знаем, как выглядит «хороший» код LLaMe; нам неизвестно, какие паттерны и идиомы окажутся полезными, а каких стоит избегать. Мы, люди, накопили опыт за многие поколения. Мы кое-что знаем о том, что работает, а что нет. Однако весь этот опыт неразрывно связан с ограничениями человеческого мышления.
LLM не смогут учиться на нашем опыте. Все наши книги, доклады на конференциях, подкасты и посты в блогах посвящены человеческим проблемам. Вряд ли они помогут избежать техдолга в коде LLaMe. LLM придется учиться на собственном опыте.
Ускорение
Учатся ли LLM на собственном опыте уже сейчас? Возможно. Они могли бы писать отчёты об инцидентах и аналитические статьи для других LLM. Они способны делать это гораздо быстрее людей, и, вероятно, можно обучать следующее поколение LLM-программистов на опыте, накопленном предыдущим поколением. Этот процесс может идти гораздо быстрее, чем усвоение подобных знаний людьми.
Так что, возможно, техдолг в LLaMe будет проблемой в течение нескольких лет, но затем и она будет решена. LLM не будут писать код по принципу YOLO (бездумно и наобум), а станут следовать надлежащим практикам программирования LLaMe.
Итого
Качество кода важно для программистов-людей из-за когнитивных ограничений. Вряд ли LLM будут скованы теми же ограничениями, что и мы, но у них могут появиться свои собственные. Такие машинные ограничения могут привести к возникновению техдолга, замедляющего работу LLM при разработке и совершенствовании ПО.
Возможно, эта проблема так и не возникнет. Или же она появится на какое-то время, а затем будет решена. А может быть, и нет. Если это произойдет, мы можем столкнуться с проблемой, которую не в силах решить. Эти ограничения будут настолько выходить за рамки человеческого понимания, что у нас не будет шансов разобраться в неполадках. Как минимум, этот мысленный эксперимент наводит на мысль, что научить LLM создавать ПО совсем без участия человека будет гораздо сложнее, чем утверждают многие оптимисты в сфере ИИ.
Источник: https://blog.ploeh.dk/2026/07/23/does-code-quality-still-matter/
Важно ли По-прежнему Качество Кода?
Автор оригинала: Марк Симан
Пока мы используем LLM как инструмент для генерации кода, за который в итоге отвечают люди, качество кода остается важным. Людям приходится проверять этот код, работать с ним, исправлять ошибки и т.п. Но что произойдёт, если люди перестанут участвовать в этом процессе? Если в будущем весь код будут писать LLM, будет ли иметь значение качество кода?
Почему качество кода имеет значение
Выражение «писать код в стиле YOLO (You Only Look Once)» означает создание кода без оглядки на его качество — практика, которая, откровенно говоря, преобладала десятилетиями. И всё же компании-разработчики, позволяющие своим сотрудникам так работать, в итоге сталкиваются с проблемами. Они накапливают столько технического долга, что больше не могут своевременно реагировать на запросы бизнеса.
Важно уточнить: качество кода — это не то же самое, что качество ПО. Качество кода — это внутреннее свойство кодовой базы: то, как код структурирован, насколько он читаем и как легко его изменять.
До сих пор все это сводилось к вопросам человеческого восприятия и мышления. Именно этому посвящена книга «Код, который умещается в голове». Код писали люди. Людям приходилось его редактировать. А что, если ситуация изменится?
Языки программирования для LLM
Представим будущее, в котором люди больше не пишут и не читают код. Важно ли в таком случае качество кода? Очевидно, что если люди перестанут работать с кодом, то все ограничения, связанные с особенностями человеческого мышления, потеряют актуальность. Полученный код может отличаться чрезмерно длинными методами, высокой цикломатической сложностью, невнятными именами переменных и сильной связностью компонентов. Более того, некоторые из этих понятий могут вообще утратить смысл. Само понятие «длинный метод» предполагает, что ПО вообще разбито на методы. Спойлер: в машинном коде их нет.
В реальности я не ожидаю, что LLM будут генерировать машинный код напрямую — хотя бы потому, что машинный код не обладает переносимостью. С другой стороны, нет оснований полагать, что в этом гипотетическом будущем LLM будут придерживаться языков, существующих сегодня. За исключением машинного кода, все языки программирования созданы для того, чтобы облегчить процесс написания кода людьми. Если люди перестанут смотреть на код, вполне вероятно появление новых языков, оптимизированных для того, чтобы LLM могли их воспринимать, генерировать и преобразовывать. Предположим, что появится один такой язык. Назовём его LLaMe.
Техдолг для LLM
Будет ли у LLM возникать техдолг? Непонятно. Возможно, нет. Тогда всё описанное далее теряет смысл: мы просто позволим LLM бесконечно генерировать код на LLaMe, не заботясь о последствиях, и это никогда не станет проблемой.
И все же я допускаю, что техдолг может накапливаться. Даже при работе с машинным кодом существуют варианты структурирования: например, можно повторно использовать регистр или задействовать два разных регистра для выполнения операции.
Следовательно, нужно исходить из того, что LLaMe будет допускать различные варианты структурирования кода. Некоторые из этих структур могут сложнее поддаваться изменениям, чем другие. Для «понимания» некоторых структур LLM может потребоваться больше ресурсов (токенов?). Хорошо структурированный код на LLaMe позволит LLM быстро и с минимальными затратами добавлять новые функции и исправлять ошибки. Улучшение же плохо структурированного кода на LLaMe потребует больше времени и ресурсов.
Могут ли LLM предотвратить появление техдолга?
Как выглядит плохой код, написанный LLM (назовем этот стиль LLaMe)? Мы даже не знаем, как выглядит «хороший» код LLaMe; нам неизвестно, какие паттерны и идиомы окажутся полезными, а каких стоит избегать. Мы, люди, накопили опыт за многие поколения. Мы кое-что знаем о том, что работает, а что нет. Однако весь этот опыт неразрывно связан с ограничениями человеческого мышления.
LLM не смогут учиться на нашем опыте. Все наши книги, доклады на конференциях, подкасты и посты в блогах посвящены человеческим проблемам. Вряд ли они помогут избежать техдолга в коде LLaMe. LLM придется учиться на собственном опыте.
Ускорение
Учатся ли LLM на собственном опыте уже сейчас? Возможно. Они могли бы писать отчёты об инцидентах и аналитические статьи для других LLM. Они способны делать это гораздо быстрее людей, и, вероятно, можно обучать следующее поколение LLM-программистов на опыте, накопленном предыдущим поколением. Этот процесс может идти гораздо быстрее, чем усвоение подобных знаний людьми.
Так что, возможно, техдолг в LLaMe будет проблемой в течение нескольких лет, но затем и она будет решена. LLM не будут писать код по принципу YOLO (бездумно и наобум), а станут следовать надлежащим практикам программирования LLaMe.
Итого
Качество кода важно для программистов-людей из-за когнитивных ограничений. Вряд ли LLM будут скованы теми же ограничениями, что и мы, но у них могут появиться свои собственные. Такие машинные ограничения могут привести к возникновению техдолга, замедляющего работу LLM при разработке и совершенствовании ПО.
Возможно, эта проблема так и не возникнет. Или же она появится на какое-то время, а затем будет решена. А может быть, и нет. Если это произойдет, мы можем столкнуться с проблемой, которую не в силах решить. Эти ограничения будут настолько выходить за рамки человеческого понимания, что у нас не будет шансов разобраться в неполадках. Как минимум, этот мысленный эксперимент наводит на мысль, что научить LLM создавать ПО совсем без участия человека будет гораздо сложнее, чем утверждают многие оптимисты в сфере ИИ.
Источник: https://blog.ploeh.dk/2026/07/23/does-code-quality-still-matter/
👍5
День 2790. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
46. AutoMapper, метод расширения или операторы неявного приведения
«Сравните использование AutoMapper, методов расширения и операторов неявного приведения для преобразования (маппинга) объектов в приложениях .NET. Обсудите преимущества и сценарии, наиболее подходящие для каждого из этих подходов. Приведите примеры реализации для каждого метода в минимальных API».
Хороший ответ
В приложениях .NET, особенно при использовании многоуровневой архитектуры, часто возникает необходимость преобразования объектов одного типа в другой. Для решения этой задачи популярны три способа: использование AutoMapper, методов расширения и операторов неявного приведения. У каждого из них есть свои преимущества и оптимальные сценарии применения.
AutoMapper — библиотека, которая автоматически преобразует объекты одного типа в другой на основе заданных конфигураций маппинга. Её лучше всего использовать в проектах, где часто требуется выполнять сложные преобразования между типами. AutoMapper значительно сокращает объём шаблонного кода. Библиотека способна автоматически обрабатывать вложенные объекты, коллекции и сложные структуры без необходимости явно описывать правила для каждого поля, как показано в следующем примере:
Методы расширения особенно полезны в сценариях сопоставления данных, когда требуется применить специфическую пользовательскую логику. Такие методы обеспечивают чёткий контроль над логикой преобразования, что критически важно в тех случаях, когда процесс трансформации не является тривиальным:
Операторы неявного приведения позволяют выполнять автоматическое преобразование между двумя типами. Это удобно, когда требуется максимально упростить преобразование одного типа в другой, избегая явных вызовов методов. Они обеспечивают бесшовную интеграцию и удобство использования в коде. При разумном применении это позволяет сделать код более чистым:
Недостатком является сильная связанность между классами
Часто встречающийся плохой ответ
«Просто используйте AutoMapper для любых задач маппинга; это всегда самый простой и эффективный способ преобразования объектов любого типа».
Почему это неверно
- Чрезмерная зависимость от сторонних инструментов: универсальная рекомендация использовать AutoMapper не учитывает ситуации, когда этот инструмент может создавать избыточные накладные расходы — особенно при простом маппинге.
- Игнорирование вопросов производительности: несмотря на свою мощь, AutoMapper может негативно влиять на производительность в высоконагруженных системах из-за затрат ресурсов на настройку и выполнение стратегий маппинга, что не всегда оправдано.
- Отсутствие индивидуального подхода: каждая задача маппинга может требовать особого решения в зависимости от сложности, требований к производительности и необходимости кастомизации. Рекомендация универсального решения свидетельствует о непонимании этих нюансов.
Подобный ответ обычно обусловлен недостаточным пониманием различных инструментов и стратегий маппинга либо стремлением найти простое решение без учёта конкретных требований и последствий для проекта.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
46. AutoMapper, метод расширения или операторы неявного приведения
«Сравните использование AutoMapper, методов расширения и операторов неявного приведения для преобразования (маппинга) объектов в приложениях .NET. Обсудите преимущества и сценарии, наиболее подходящие для каждого из этих подходов. Приведите примеры реализации для каждого метода в минимальных API».
Хороший ответ
В приложениях .NET, особенно при использовании многоуровневой архитектуры, часто возникает необходимость преобразования объектов одного типа в другой. Для решения этой задачи популярны три способа: использование AutoMapper, методов расширения и операторов неявного приведения. У каждого из них есть свои преимущества и оптимальные сценарии применения.
AutoMapper — библиотека, которая автоматически преобразует объекты одного типа в другой на основе заданных конфигураций маппинга. Её лучше всего использовать в проектах, где часто требуется выполнять сложные преобразования между типами. AutoMapper значительно сокращает объём шаблонного кода. Библиотека способна автоматически обрабатывать вложенные объекты, коллекции и сложные структуры без необходимости явно описывать правила для каждого поля, как показано в следующем примере:
var builder = WebApplication.CreateBuilder(args);
// Предположим, маппинги определены в Program.cs
builder.Services.AddAutoMapper(typeof(Program));
var app = builder.Build();
app.MapGet("/customer/{id}", async
(int id, IMapper mapper, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
CustomerDto customerDto = mapper.Map<CustomerDto>(customer);
return Results.Ok(customerDto);
});
app.Run();
Методы расширения особенно полезны в сценариях сопоставления данных, когда требуется применить специфическую пользовательскую логику. Такие методы обеспечивают чёткий контроль над логикой преобразования, что критически важно в тех случаях, когда процесс трансформации не является тривиальным:
public static class MappingExtensions
{
public static CustomerDto ToDto(
this Customer customer)
{
// … сложная логика преобразования …
return new CustomerDto
{
Id = customer.Id,
Name = customer.Name,
//…
};
}
}
//…
app.MapGet("/customer/{id}",
async (int id, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
CustomerDto customerDto = customer.ToDto();
return Results.Ok(customerDto);
});
Операторы неявного приведения позволяют выполнять автоматическое преобразование между двумя типами. Это удобно, когда требуется максимально упростить преобразование одного типа в другой, избегая явных вызовов методов. Они обеспечивают бесшовную интеграцию и удобство использования в коде. При разумном применении это позволяет сделать код более чистым:
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
//…
public static implicit operator
CustomerDto(Customer customer)
{
return new CustomerDto
{
Id = customer.Id,
Name = customer.Name,
//…
};
}
}
//…
app.MapGet("/customer/{id}",
async (int id, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
//неявное приведение
CustomerDto customerDto = customer;
return Results.Ok(customerDto);
});
Недостатком является сильная связанность между классами
CustomerDto и Customer.Часто встречающийся плохой ответ
«Просто используйте AutoMapper для любых задач маппинга; это всегда самый простой и эффективный способ преобразования объектов любого типа».
Почему это неверно
- Чрезмерная зависимость от сторонних инструментов: универсальная рекомендация использовать AutoMapper не учитывает ситуации, когда этот инструмент может создавать избыточные накладные расходы — особенно при простом маппинге.
- Игнорирование вопросов производительности: несмотря на свою мощь, AutoMapper может негативно влиять на производительность в высоконагруженных системах из-за затрат ресурсов на настройку и выполнение стратегий маппинга, что не всегда оправдано.
- Отсутствие индивидуального подхода: каждая задача маппинга может требовать особого решения в зависимости от сложности, требований к производительности и необходимости кастомизации. Рекомендация универсального решения свидетельствует о непонимании этих нюансов.
Подобный ответ обычно обусловлен недостаточным пониманием различных инструментов и стратегий маппинга либо стремлением найти простое решение без учёта конкретных требований и последствий для проекта.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎9👍2
День 2791. #ЗаметкиНаПолях #SQL
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 1
Большинство разработчиков используют лишь 20% возможностей SQL. SELECT, JOIN, GROUP BY — и на этом останавливаются. Однако у SQL есть и «второй уровень» — функции, позволяющие превратить страницу кода приложения или три отдельных запроса в одну лаконичную и понятную инструкцию. При этом они не являются новыми или экзотическими: они уже доступны в используемой вами БД.
Замечание: запросы были протестированы в БД PostgreSQL. Большинство описанных функций поддерживаются и другими СУБД, хотя синтаксис может различаться.
1. Обобщённые табличные выражения (CTE)
Сложный запрос, оформленный как единая инструкция, труден для чтения, а вносить в него изменения ещё сложнее. CTE позволяет разбить его на последовательные именованные этапы с помощью ключевого слова WITH. Каждый этап представляет собой временный именованный набор данных, который можно использовать в дальнейшем ходе запроса:
Здесь 2 именованные части:
-
-
В результате получается запрос, который читается сверху вниз, в отличие от подхода с использованием вложенных подзапросов, которые приходится разбирать изнутри наружу.
CTE также поддерживают рекурсию (с помощью конструкции
CTE можно использовать в операторах
2. Оконные функции
Иногда требуется выполнить вычисления по связанным строкам, сохранив при этом в результирующем наборе каждую отдельную строку. Оператор GROUP BY сворачивает строки, оставляя по одной записи на группу. Оконная функция же выполняет вычисления по набору строк (так называемому «окну»), не объединяя при этом сами строки:
Первый запрос ранжирует отправления каждого перевозчика по дате.
Во втором запросе используются
Оконные функции позволяют вычислять нарастающие итоги и скользящие средние, определять ранги, а также сравнивать данные в разных строках.
Они являются стандартом SQL и поддерживаются в PostgreSQL, SQL Server, Oracle и MySQL 8+.
3. LATERAL-Соединения
Обычное соединение соединяет две таблицы на основе определённого условия. LATERAL-соединение позволяет подзапросу, расположенному справа, обращаться к столбцам таблицы, расположенной слева, и выполняется для каждой строки отдельно, что идеально подходит для задач типа «выбрать N лучших записей в каждой группе»:
Для каждого конкретного перевозчика коррелирующий подзапрос выбирает одну — самую свежую — поставку (с помощью
Ключевой момент — условие
Это наиболее элегантный способ получить «самую свежую запись в группе» или «топ-3 записи в категории» без использования оконных функций.
Примечание: в SQL Server аналогичная задача решается с помощью
Продолжение следует…
Источник: https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 1
Большинство разработчиков используют лишь 20% возможностей SQL. SELECT, JOIN, GROUP BY — и на этом останавливаются. Однако у SQL есть и «второй уровень» — функции, позволяющие превратить страницу кода приложения или три отдельных запроса в одну лаконичную и понятную инструкцию. При этом они не являются новыми или экзотическими: они уже доступны в используемой вами БД.
Замечание: запросы были протестированы в БД PostgreSQL. Большинство описанных функций поддерживаются и другими СУБД, хотя синтаксис может различаться.
1. Обобщённые табличные выражения (CTE)
Сложный запрос, оформленный как единая инструкция, труден для чтения, а вносить в него изменения ещё сложнее. CTE позволяет разбить его на последовательные именованные этапы с помощью ключевого слова WITH. Каждый этап представляет собой временный именованный набор данных, который можно использовать в дальнейшем ходе запроса:
WITH recent_shipments AS (
SELECT id, number, carrier, status, created_at
FROM shipments
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
),
shipment_details AS (
SELECT rs.number, rs.carrier, rs.status,
COUNT(si.id) AS total_items,
SUM(si.quantity) AS total_quantity
FROM recent_shipments rs
LEFT JOIN shipment_items si ON rs.id = si.shipment_id
GROUP BY rs.number, rs.carrier, rs.status
)
SELECT number, carrier, status, total_items, total_quantity
FROM shipment_details ORDER BY total_quantity DESC;
Здесь 2 именованные части:
-
recent_shipments выбирает данные об поставках за последние 30 дней;-
shipment_details использует этот результат, присоединяя к нему данные о товарных позициях и выполняя агрегацию (подсчёт количества и объёмов). В итоговом операторе SELECT обращение ко второму CTE происходит так же, как к обычной таблице.В результате получается запрос, который читается сверху вниз, в отличие от подхода с использованием вложенных подзапросов, которые приходится разбирать изнутри наружу.
CTE также поддерживают рекурсию (с помощью конструкции
WITH RECURSIVE); это позволяет работать с иерархическими данными, такими как организационные структуры или деревья категорий.CTE можно использовать в операторах
SELECT, INSERT, UPDATE или DELETE.2. Оконные функции
Иногда требуется выполнить вычисления по связанным строкам, сохранив при этом в результирующем наборе каждую отдельную строку. Оператор GROUP BY сворачивает строки, оставляя по одной записи на группу. Оконная функция же выполняет вычисления по набору строк (так называемому «окну»), не объединяя при этом сами строки:
SELECT number, carrier, created_at,
ROW_NUMBER() OVER (PARTITION BY carrier ORDER BY created_at DESC) AS shipment_sequence,
RANK() OVER (PARTITION BY carrier ORDER BY created_at DESC) AS shipment_rank
FROM shipments;
SELECT number, status, created_at,
LAG(status) OVER (ORDER BY created_at) AS previous_status,
LEAD(carrier) OVER (ORDER BY created_at) AS next_carrier
FROM shipments;
Первый запрос ранжирует отправления каждого перевозчика по дате.
ROW_NUMBER() присваивает уникальный порядковый номер в рамках группы (заданной через PARTITION BY carrier), а RANK() делает то же самое, но при совпадении значений присваивает им одинаковый ранг.Во втором запросе используются
LAG и LEAD для обращения к предыдущей и следующей строкам (в данном случае — для получения предыдущего статуса и следующего перевозчика) без выполнения самосоединения (self-join).Оконные функции позволяют вычислять нарастающие итоги и скользящие средние, определять ранги, а также сравнивать данные в разных строках.
Они являются стандартом SQL и поддерживаются в PostgreSQL, SQL Server, Oracle и MySQL 8+.
3. LATERAL-Соединения
Обычное соединение соединяет две таблицы на основе определённого условия. LATERAL-соединение позволяет подзапросу, расположенному справа, обращаться к столбцам таблицы, расположенной слева, и выполняется для каждой строки отдельно, что идеально подходит для задач типа «выбрать N лучших записей в каждой группе»:
-- Для каждого перевозчика выбираем одну последнюю отправку
SELECT c.carrier, s.number, s.status, s.created_at
FROM (SELECT DISTINCT carrier FROM shipments) c
CROSS JOIN LATERAL (
SELECT number, status, created_at
FROM shipments WHERE carrier = c.carrier
ORDER BY created_at DESC LIMIT 1
) s;
Для каждого конкретного перевозчика коррелирующий подзапрос выбирает одну — самую свежую — поставку (с помощью
ORDER BY created_at DESC LIMIT 1).Ключевой момент — условие
WHERE carrier = c.carrier: внутренний запрос «видит» значение carrier из строки внешнего запроса, чего обычный подзапрос сделать не может.Это наиболее элегантный способ получить «самую свежую запись в группе» или «топ-3 записи в категории» без использования оконных функций.
Примечание: в SQL Server аналогичная задача решается с помощью
CROSS APPLY (или OUTER APPLY для аналога LEFT JOIN), а в PostgreSQL используются CROSS JOIN LATERAL и LEFT JOIN LATERAL.Продолжение следует…
Источник: https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
👍9👎1
22 сентября(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle C# разработчика.
Как это будет:
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для C# разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_csharp_bot
Реклама.
О рекламодателе.
Please open Telegram to view this post
VIEW IN TELEGRAM
👎5👍2
День 2792. #ЗаметкиНаПолях #SQL
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 2
1-3
4. GROUPING SETS, ROLLUP и CUBE
Для отчёта часто требуется получить сразу несколько уровней агрегации: итоговые значения по перевозчику и статусу, промежуточные итоги по перевозчику и общий итог. Наивный подход предполагает объединение нескольких запросов с помощью оператора
Первый запрос формирует группировки, которые вам нужны: по перевозчику и статусу, только по перевозчику, только по статусу, а также пустую группу
Оператор
Оператор
Один запрос заменяет 4, а БД вычисляет уровни за один проход, вместо того чтобы многократно сканировать таблицу.
Эти средства являются частью стандарта SQL и поддерживаются в PostgreSQL, SQL Server и Oracle.
5. Предложение FILTER в агрегатных функциях
Часто возникает необходимость подсчитать количество или сумму только для тех строк, которые удовлетворяют определённому условию, и вывести эти результаты рядом друг с другом. Предложение
Каждое выражение
Запись
Назначение конструкции
Примечание:
6. UPSERT (INSERT … ON CONFLICT)
Вставка строки, если она новая, и её обновление, если она уже существует — распространённая задача, для решения которой обычно требуются
Команда
Псевдотаблица
Выражение
Одна команда, никаких дублирующихся строк и никаких проблем с состоянием гонки при одновременном выполнении запросов разными клиентами.
Примечание: это синтаксис PostgreSQL. В стандарте SQL (и в таких СУБД, как SQL Server или Oracle) используется оператор
Замечание: поле, по которому будет отслеживаться конфликт (
7. Поддержка JSON
Иногда требуется хранить гибкие, полуструктурированные данные — например, событие, тело веб-хука или блок настроек. PostgreSQL поддерживает JSON на уровне ядра (тип JSONB) и позволяет выполнять запросы к содержимому таких полей; благодаря этому вам не нужна отдельная документоориентированная БД для редких случаев использования JSON. Также отпадает необходимость хранить JSON в виде обычных строк и обрабатывать их на стороне бэкенда, теряя при этом все преимущества индексации:
В таблице событий данные хранятся в формате JSONB. Запрос обращается к ним следующим образом: оператор
Данные JSONB хранятся в разобранном бинарном виде и поддерживают индексацию, что позволяет выполнять фильтрацию и извлечение информации без сканирования документов целиком.
Примечание: в SQL Server для работы с JSON используются функции
Окончание следует…
Источник: https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 2
1-3
4. GROUPING SETS, ROLLUP и CUBE
Для отчёта часто требуется получить сразу несколько уровней агрегации: итоговые значения по перевозчику и статусу, промежуточные итоги по перевозчику и общий итог. Наивный подход предполагает объединение нескольких запросов с помощью оператора
UNION ALL. Конструкции GROUPING SETS, ROLLUP и CUBE позволяют получить все эти уровни в рамках одного запроса:SELECT carrier, status,
COUNT(*) AS shipment_count,
SUM(si.quantity) AS total_quantity
FROM shipments s
LEFT JOIN shipment_items si ON s.id = si.shipment_id
GROUP BY GROUPING SETS (
(carrier, status), -- по поставщику и статусу
(carrier), -- подытог по поставщику
(status), -- подытог по статусу
() -- общий итог
);
SELECT carrier, status,
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS shipment_count
FROM shipments
GROUP BY ROLLUP (carrier, status,
DATE_TRUNC('month', created_at)
);
Первый запрос формирует группировки, которые вам нужны: по перевозчику и статусу, только по перевозчику, только по статусу, а также пустую группу
() для получения общего итога.Оператор
ROLLUP во втором запросе — это сокращённая запись для иерархических промежуточных итогов: сначала по перевозчику, затем по перевозчику и статусу, далее по перевозчику, статусу и месяцу — и наконец общий итог.Оператор
CUBE создает все возможные комбинации столбцов.Один запрос заменяет 4, а БД вычисляет уровни за один проход, вместо того чтобы многократно сканировать таблицу.
Эти средства являются частью стандарта SQL и поддерживаются в PostgreSQL, SQL Server и Oracle.
5. Предложение FILTER в агрегатных функциях
Часто возникает необходимость подсчитать количество или сумму только для тех строк, которые удовлетворяют определённому условию, и вывести эти результаты рядом друг с другом. Предложение
FILTER применяет условие к конкретной агрегатной функции, благодаря чему каждая из них обрабатывает своё подмножество данных — в рамках одной строки и за один проход по данным:SELECT carrier,
COUNT(*) AS total_shipments,
COUNT(*) FILTER (WHERE status = 'delivered') AS delivered_count,
COUNT(*) FILTER (WHERE status = 'in_transit') AS in_transit_count,
COUNT(*) FILTER (WHERE status = 'pending') AS pending_count,
SUM(si.quantity) FILTER (WHERE status = 'delivered') AS delivered_quantity,
SUM(si.quantity) FILTER (WHERE status = 'pending') AS pending_quantity
FROM shipments s
LEFT JOIN shipment_items si ON s.id = si.shipment_id
GROUP BY carrier;
Каждое выражение
COUNT(*) FILTER (WHERE …) подсчитывает только соответствующие условию строки, благодаря чему вы получаете количество отправлений со статусами «доставлено», «в пути» и «в ожидании» в виде отдельных столбцов для каждого перевозчика.Запись
COUNT(*) FILTER (WHERE status = 'delivered') читается легче, чем старый приём с использованием CASE: SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END).Назначение конструкции
FILTER гораздо более очевидно.Примечание:
FILTER поддерживается в PostgreSQL. В SQL Server и MySQL такой возможности нет — там приходится использовать CASE внутри агрегатной функции, например: COUNT(CASE WHEN status = 'delivered' THEN 1 END).6. UPSERT (INSERT … ON CONFLICT)
Вставка строки, если она новая, и её обновление, если она уже существует — распространённая задача, для решения которой обычно требуются
SELECT, условие и две ветви выполнения кода. Операция UPSERT позволяет выполнить это одной атомарной командой, исключая риск возникновения состояния гонки между проверкой и записью:INSERT INTO shipments (id, number, order_id, address_street, address_city, address_zip, carrier, receiver_email, status, created_at, updated_at)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 'SH-2024-001', 'ORD-2024-001', '123 Main St', 'New York', '10001', 'FedEx', 'customer@example.com', 'pending', NOW(), NOW())
ON CONFLICT (number) DO UPDATE
SET
carrier = EXCLUDED.carrier,
status = EXCLUDED.status,
updated_at = GREATEST(shipments.updated_at, EXCLUDED.updated_at);
Команда
INSERT … ON CONFLICT (number) DO UPDATE пытается выполнить вставку; если строка с таким номером уже существует, вместо этого выполняется обновление.Псевдотаблица
EXCLUDED содержит значения, которые вы пытались вставить, поэтому запись carrier = EXCLUDED.carrier означает «использовать нового перевозчика».Выражение
GREATEST(shipments.updated_at, EXCLUDED.updated_at) позволяет сохранить более позднюю из двух временных меток.Одна команда, никаких дублирующихся строк и никаких проблем с состоянием гонки при одновременном выполнении запросов разными клиентами.
Примечание: это синтаксис PostgreSQL. В стандарте SQL (и в таких СУБД, как SQL Server или Oracle) используется оператор
MERGE, а в MySQL — INSERT … ON DUPLICATE KEY UPDATE.Замечание: поле, по которому будет отслеживаться конфликт (
number) должно иметь ограничение уникальности.7. Поддержка JSON
Иногда требуется хранить гибкие, полуструктурированные данные — например, событие, тело веб-хука или блок настроек. PostgreSQL поддерживает JSON на уровне ядра (тип JSONB) и позволяет выполнять запросы к содержимому таких полей; благодаря этому вам не нужна отдельная документоориентированная БД для редких случаев использования JSON. Также отпадает необходимость хранить JSON в виде обычных строк и обрабатывать их на стороне бэкенда, теряя при этом все преимущества индексации:
CREATE TABLE ship_events (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL
);
-- Пример данных
INSERT INTO ship_events (payload) VALUES
('{"type":"click","coordinates":[{"x":10,"y":20},{"x":15,"y":25}]}'),
('{"type":"hover","coordinates":[{"x":5,"y":30}]}'),
('{"type":"scroll","coordinates":[{"x":0,"y":100},{"x":0,"y":200},{"x":0,"y":300}]}');
-- Выбираем поля JSON
SELECT
payload ->> 'type' AS event_type,
payload -> 'coordinates' -> 0 ->> 'x' AS first_x,
payload -> 'coordinates' -> 0 ->> 'y' AS first_y
FROM ship_events;
В таблице событий данные хранятся в формате JSONB. Запрос обращается к ним следующим образом: оператор
->> извлекает значение как текст, а оператор -> — вложенный JSON-объект или элемент массива; так, выражение payload -> 'coordinates' -> 0 ->> 'x' позволяет получить координату x первого элемента.Данные JSONB хранятся в разобранном бинарном виде и поддерживают индексацию, что позволяет выполнять фильтрацию и извлечение информации без сканирования документов целиком.
Примечание: в SQL Server для работы с JSON используются функции
JSON_VALUE и OPENJSON, а стандарт SQL предусматривает функцию JSON_TABLE (доступную в Oracle, MySQL и PostgreSQL 17+), которая преобразует JSON-массив непосредственно в строки реляционной таблицы.Окончание следует…
Источник: https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
👍9