Хорошие новости для пользователей Postgres под конец года. Команда TimescaleDB выпустила и открыла исходники расширения pg_textsearch.
В Postgres уже есть встроенный полнотекстовый поиск, а это расширение делает его более современным и продвинутым за счёт добавления ранжирования BM25.
Если тебе важен быстрый полнотекстовый поиск с нормальной релевантностью без выхода из Postgres, или гибридный retrieval с комбинацией pg_textsearch и pgvector / pgvectorscale — это точно для тебя.
https://github.com/timescale/pg_textsearch
👉 @KodBlog
В Postgres уже есть встроенный полнотекстовый поиск, а это расширение делает его более современным и продвинутым за счёт добавления ранжирования BM25.
Если тебе важен быстрый полнотекстовый поиск с нормальной релевантностью без выхода из Postgres, или гибридный retrieval с комбинацией pg_textsearch и pgvector / pgvectorscale — это точно для тебя.
https://github.com/timescale/pg_textsearch
Please open Telegram to view this post
VIEW IN TELEGRAM
Tiger Data Blog
From ts_rank to BM25. Introducing pg_textsearch: True BM25 Ranking and Hybrid Retrieval Inside Postgres | Tiger Data
pg_textsearch brings BM25 ranking to enable hybrid search to Postgres. Build RAG systems with keyword precision and vector semantics in one database.
👍5❤3
This media is not supported in your browser
VIEW IN TELEGRAM
Помогите… мой VS Code захватили покемоны 😆
Это расширение для VS Code добавляет покемонов прямо внутрь редактора.
Пока ты пишешь код или смотришь, как AI генерит его за тебя, эти покемоны просто тусуются и бегают по экрану, создавая компанию и немного настроения
👉 @KodBlog
Это расширение для VS Code добавляет покемонов прямо внутрь редактора.
Пока ты пишешь код или смотришь, как AI генерит его за тебя, эти покемоны просто тусуются и бегают по экрану, создавая компанию и немного настроения
Please open Telegram to view this post
VIEW IN TELEGRAM
😁14🥰10❤4👎1
C# Portal | Программирование
Курсорная пагинация использует контрольную точку (курсор), чтобы получить следующий набор результатов. Эта контрольная точка обычно представляет собой уникальный идентификатор или комбинацию полей, задающих порядок сортировки. Используем поля Date и Id, чтобы…
Улучшаем курсорную пагинацию
Чтобы ускорить пагинацию, можно добавить индекс:
Индекс создаётся в обратном порядке, чтобы соответствовать ORDER BY в запросах. Однако если посмотреть план выполнения, окажется, что индекс используется, но сам запрос может работать даже медленнее, чем без него:
Возможно, объём данных слишком мал, чтобы индекс дал выигрыш. Но есть один трюк — сравнение кортежей:
В этом варианте индекс отрабатывает корректно и заметно ускоряет выполнение. Оптимизатор запросов не всегда может понять, что составной индекс можно использовать для построчного сравнения, но при сравнении кортежей индекс применяется эффективно.
У провайдера EF для Postgres есть метод EF.Functions.LessThanOrEqual, который принимает ValueTuple. Его можно использовать для построения такого сравнения:
» Кодирование курсора
Курсор, используемый для получения следующей страницы результатов, можно закодировать. Клиенту он отдаётся в виде строки Base64, без знания внутренней структуры:
Пример использования:
👉 @KodBlog
Чтобы ускорить пагинацию, можно добавить индекс:
CREATE INDEX idx_user_notes_date_id
ON user_notes (date DESC, id DESC);
Индекс создаётся в обратном порядке, чтобы соответствовать ORDER BY в запросах. Однако если посмотреть план выполнения, окажется, что индекс используется, но сам запрос может работать даже медленнее, чем без него:
EXPLAIN ANALYZE
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
WHERE u.date < @date
OR (u.date = @date AND u.id <= @lastId)
ORDER BY u.date DESC, u.id DESC
LIMIT 1000;
Возможно, объём данных слишком мал, чтобы индекс дал выигрыш. Но есть один трюк — сравнение кортежей:
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
WHERE (u.date, u.id) <= (@date, @lastId)
ORDER BY u.date DESC, u.id DESC
LIMIT 1000;
В этом варианте индекс отрабатывает корректно и заметно ускоряет выполнение. Оптимизатор запросов не всегда может понять, что составной индекс можно использовать для построчного сравнения, но при сравнении кортежей индекс применяется эффективно.
У провайдера EF для Postgres есть метод EF.Functions.LessThanOrEqual, который принимает ValueTuple. Его можно использовать для построения такого сравнения:
query = query.Where(x => EF.Functions.LessThanOrEqual(
ValueTuple.Create(x.Date, x.Id),
ValueTuple.Create(date, lastId)));
» Кодирование курсора
Курсор, используемый для получения следующей страницы результатов, можно закодировать. Клиенту он отдаётся в виде строки Base64, без знания внутренней структуры:
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
public record Cursor(DateOnly Date, Guid LastId)
{
public string Encode() =>
Base64Url.EncodeToString(
Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(this)));
public static Cursor? Decode(string? cursor)
{
if (string.IsNullOrWhiteSpace(cursor))
return null;
try
{
return JsonSerializer.Deserialize<Cursor>(
Encoding.UTF8.GetString(
Base64Url.DecodeFromChars(cursor)));
}
catch
{
return null;
}
}
}
Пример использования:
var cursor = new Cursor(
new DateOnly(2025, 4, 26),
Guid.Parse("019500f9-8b41-74cf-ab12-25a48d4d4ab4"));
var encoded = cursor.Encode();
// eyJEYXRlIjoiMjAyNS0wMi0xNSIsIkxhc3RJZCI6IjAxOTUwMGY5LThiNDEtNzRjZi1hYjEyLTI1YTQ4ZDRkNGFiNCJ9
var decoded = Cursor.Decode(encoded);
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍3❤2
Твои EF Core-запросы могут спокойно убивать производительность API
И обычно все валят на архитектуру или базу, хотя проблема сидит прямо в DAL.
Что вижу почти в каждом код-ревью:
1. Тащишь целые сущности, хотя нужно 2 поля
Плохо:
Норм:
Вместо 15 колонок забрал 2.
2. Классика N+1
Идешь по заказам и для каждого дергаешь клиента из БД?
Получаешь 100 запросов вместо 1.
Решение:
3. Нет
Зачем трекать изменения, если просто читаешь?
Добавь
4. Забываешь про индексы
EF Core не сделает все индексы за тебя.
Фильтруешь по
5. Слишком рано делаешь
Так ты сначала грузишь все в память, а потом фильтруешь в C#, а не в SQL.
Держи запрос
Большинство “медленных API” - это не архитектура.
Это мелкие решения, которые накапливаются.
Перед тем как переписывать всё, вычисти эти паттерны.
👉 @KodBlog
И обычно все валят на архитектуру или базу, хотя проблема сидит прямо в DAL.
Что вижу почти в каждом код-ревью:
1. Тащишь целые сущности, хотя нужно 2 поля
Плохо:
var users = await context.Users.ToListAsync();
Норм:
var users = await context.Users
.Select(u => new { u.Id, u.Name })
.ToListAsync();
Вместо 15 колонок забрал 2.
2. Классика N+1
Идешь по заказам и для каждого дергаешь клиента из БД?
Получаешь 100 запросов вместо 1.
Решение:
Include() или проекции в Select().3. Нет
AsNoTracking() на read-onlyЗачем трекать изменения, если просто читаешь?
Добавь
AsNoTracking() и снизишь расход памяти/CPU.4. Забываешь про индексы
EF Core не сделает все индексы за тебя.
Фильтруешь по
OrderDate или CustomerId? Добавь индекс.5. Слишком рано делаешь
ToList()Так ты сначала грузишь все в память, а потом фильтруешь в C#, а не в SQL.
Держи запрос
IQueryable, пусть БД делает работу.Большинство “медленных API” - это не архитектура.
Это мелкие решения, которые накапливаются.
Перед тем как переписывать всё, вычисти эти паттерны.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤2
Visual Studio отличный инструмент для анализа кода
Как быстро найти проблемные места:
Анализ → Вычислить метрики кода
Инструмент проверит все решения и покажет метрики.
На что смотреть в отчёте:
- классы с большим количеством строк
- низкий Maintainability Index
- высокая цикломатическая сложность
Дальше сравните эти классы с теми, что вы уже отметили ранее.
Пересечение списков » приоритетные кандидаты для тестов и рефакторинга.
Код говорит сам за себя.
👉 @KodBlog
Как быстро найти проблемные места:
Анализ → Вычислить метрики кода
Инструмент проверит все решения и покажет метрики.
На что смотреть в отчёте:
- классы с большим количеством строк
- низкий Maintainability Index
- высокая цикломатическая сложность
Дальше сравните эти классы с теми, что вы уже отметили ранее.
Пересечение списков » приоритетные кандидаты для тестов и рефакторинга.
Код говорит сам за себя.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6
Теперь вам не нужно использовать zig, чтобы получить comptime, теперь вы можете сделать это в C#! 😁
https://github.com/sebastienros/comptime
comptime — экспериментальный проект для C#, который позволяет выполнять код на этапе компиляции.
Реализован как генератор источников: методы, помеченные атрибутом [Comptime], исполняются во время сборки, а их результат встраивается в сгенерированный C#-код. В итоге тяжёлые вычисления уезжают из runtime в build time.
Работает на .NET 8 и C# 12, с ограничениями: методы static, классы partial, аргументы — компиляторные константы.
👉 @KodBlog
https://github.com/sebastienros/comptime
comptime — экспериментальный проект для C#, который позволяет выполнять код на этапе компиляции.
Реализован как генератор источников: методы, помеченные атрибутом [Comptime], исполняются во время сборки, а их результат встраивается в сгенерированный C#-код. В итоге тяжёлые вычисления уезжают из runtime в build time.
Работает на .NET 8 и C# 12, с ограничениями: методы static, классы partial, аргументы — компиляторные константы.
Please open Telegram to view this post
VIEW IN TELEGRAM
GitHub
GitHub - sebastienros/comptime: Comptime brings meta-programming capabilities to C#, enabling compile-time code generation and…
Comptime brings meta-programming capabilities to C#, enabling compile-time code generation and evaluation. - sebastienros/comptime
❤8🔥3
DbContext не потокобезопасен. Как правильно распараллеливать запросы в EF Core.
Все мы писали эндпоинты вроде панели мониторинга или пользовательской сводки. Это такие точки, где нужно собрать несколько вообще не связанных между собой наборов данных, чтобы показать пользователю целостную картину. Например: последние 50 заказов, текущие системные логи, настройки профиля и, допустим, счётчик уведомлений.
Обычно код выглядит так:
Код чистый, читаемый, всё работает. Но есть нюанс. Если GetRecentOrdersAsync выполняется 300 мс, GetSystemLogsAsync — 400 мс, а GetUserStatsAsync — 300 мс, то общее время загрузки будет около секунды (300 + 400 + 300).
В распределённых системах лишняя задержка напрямую бьёт по пользовательскому опыту. Эти наборы данных никак не зависят друг от друга, значит, логично выполнять их параллельно. Тогда итоговое время будет равно самому медленному запросу — 400 мс. Это примерно 60% прироста производительности просто за счёт способа выполнения кода. Но с EF Core наивный подход не сработает.
» Проблема с Task.WhenAll
Самая частая ошибка — обернуть вызовы репозитория в задачи и ждать их одновременно:
При выполнении вы получите исключение:
(«Вторая операция в этом контексте началась до завершения предыдущей. Обычно это происходит из-за использования одного экземпляра DbContext из разных потоков, при том что его члены не являются потокобезопасными.»)
Почему так происходит ??
DbContext в EF Core не потокобезопасен. Это stateful-объект, рассчитанный на одну единицу работы. Он:
1. ведёт change tracking для загруженных сущностей
2. инкапсулирует одно соединение с базой данных
Протоколы БД (будь то TCP-соединение с PostgreSQL или SQL Server) по сути синхронны на уровне соединения. Нельзя одновременно отправить два разных SQL-запроса по одному и тому же каналу в один и тот же момент времени.
Когда вы используете Task.WhenAll, несколько потоков пытаются параллельно использовать одно и то же соединение. EF Core это отслеживает и намеренно выбрасывает исключение, чтобы не допустить некорректного состояния и повреждения данных.
В итоге получается дилемма » параллелизм нужен для скорости, но DbContext заставляет нас выполнять всё последовательно.
👉 @KodBlog
Все мы писали эндпоинты вроде панели мониторинга или пользовательской сводки. Это такие точки, где нужно собрать несколько вообще не связанных между собой наборов данных, чтобы показать пользователю целостную картину. Например: последние 50 заказов, текущие системные логи, настройки профиля и, допустим, счётчик уведомлений.
Обычно код выглядит так:
var orders = await GetRecentOrdersAsync(id);
var logs = await GetSystemLogsAsync();
var stats = await GetUserStatsAsync(id);
return new DashboardDto(orders, logs, stats);
Код чистый, читаемый, всё работает. Но есть нюанс. Если GetRecentOrdersAsync выполняется 300 мс, GetSystemLogsAsync — 400 мс, а GetUserStatsAsync — 300 мс, то общее время загрузки будет около секунды (300 + 400 + 300).
В распределённых системах лишняя задержка напрямую бьёт по пользовательскому опыту. Эти наборы данных никак не зависят друг от друга, значит, логично выполнять их параллельно. Тогда итоговое время будет равно самому медленному запросу — 400 мс. Это примерно 60% прироста производительности просто за счёт способа выполнения кода. Но с EF Core наивный подход не сработает.
» Проблема с Task.WhenAll
Самая частая ошибка — обернуть вызовы репозитория в задачи и ждать их одновременно:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ
public async Task<DashboardData> GetDashboardData(int id)
{
// Все методы используют один и тот же _dbContext
var orders = _repo.GetOrdersAsync(id);
var logs = _repo.GetLogsAsync();
var stats = _repo.GetStatsAsync(id);
await Task.WhenAll(orders, logs, stats);
return new DashboardData(
orders.Result,
logs.Result,
stats.Result);
}
При выполнении вы получите исключение:
A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe.
(«Вторая операция в этом контексте началась до завершения предыдущей. Обычно это происходит из-за использования одного экземпляра DbContext из разных потоков, при том что его члены не являются потокобезопасными.»)
Почему так происходит ??
DbContext в EF Core не потокобезопасен. Это stateful-объект, рассчитанный на одну единицу работы. Он:
1. ведёт change tracking для загруженных сущностей
2. инкапсулирует одно соединение с базой данных
Протоколы БД (будь то TCP-соединение с PostgreSQL или SQL Server) по сути синхронны на уровне соединения. Нельзя одновременно отправить два разных SQL-запроса по одному и тому же каналу в один и тот же момент времени.
Когда вы используете Task.WhenAll, несколько потоков пытаются параллельно использовать одно и то же соединение. EF Core это отслеживает и намеренно выбрасывает исключение, чтобы не допустить некорректного состояния и повреждения данных.
В итоге получается дилемма » параллелизм нужен для скорости, но DbContext заставляет нас выполнять всё последовательно.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤨7❤4👍3
C# Portal | Программирование
DbContext не потокобезопасен. Как правильно распараллеливать запросы в EF Core. Все мы писали эндпоинты вроде панели мониторинга или пользовательской сводки. Это такие точки, где нужно собрать несколько вообще не связанных между собой наборов данных, чтобы…
Как правильно распараллеливать запросы в EF Core.
Начиная с .NET 5, в EF Core есть нормальное решение для этого сценария — IDbContextFactory<T>.
Вместо того чтобы внедрять DbContext со scoped-временем жизни (который живёт весь HTTP-запрос), вы внедряете фабрику. Она позволяет создавать лёгкие, независимые экземпляры DbContext ровно там, где они нужны.
Примечание: фабрика самый чистый вариант с точки зрения DI, но при желании можно и вручную создавать контекст (new AppDbContext(options)), если у вас есть доступ к DbContextOptions.
Сначала регистрируем фабрику в Program.cs:
Теперь перепишем конечную точку, используя фабрику контекстов.
Внутри метода мы запускаем отдельную задачу на каждый запрос.
В каждой задаче создаётся свой DbContext, выполняется запрос и сразу же освобождается.
Ключевые моменты
1. Изоляция
Каждая задача получает свой DbContext.
Соответственно, у каждой задачи своё соединение с БД.
Никаких конфликтов.
2. Освобождение ресурсов
await using здесь критически важен.
Как только запрос завершился, контекст сразу освобождается, а соединение возвращается в пул.
При последовательном выполнении каждая операция ждёт завершения предыдущей.
При параллельном подходе все три запроса к БД стартуют и завершаются одновременно, что уменьшает общее время ответа.
Компромиссы и выводы
IDbContextFactory хорошо закрывает разрыв между unit-of-work-архитектурой EF Core и реальными требованиями к параллельной обработке.
Он позволяет выйти за рамки принципа «один запрос — один поток» без нарушения безопасности.
Но использовать этот подход нужно с головой:
1. Истощение пула соединений
Один HTTP-запрос теперь может одновременно занять 3 соединения с БД вместо одного.
При высокой нагрузке пул соединений можно выбить довольно быстро.
2. Накладные расходы
Если запросы очень быстрые (например, простой поиск по ID), накладные расходы на создание контекстов и задач могут сделать параллельный вариант медленнее последовательного.
Итог:
Если панель мониторинга работает медленно, не спешите сразу писать чистый SQL.
Посмотрите, не выполняются ли независимые запросы последовательно.
В таких случаях параллелизация через IDbContextFactory может дать заметный прирост.
👉 @KodBlog
Начиная с .NET 5, в EF Core есть нормальное решение для этого сценария — IDbContextFactory<T>.
Вместо того чтобы внедрять DbContext со scoped-временем жизни (который живёт весь HTTP-запрос), вы внедряете фабрику. Она позволяет создавать лёгкие, независимые экземпляры DbContext ровно там, где они нужны.
Примечание: фабрика самый чистый вариант с точки зрения DI, но при желании можно и вручную создавать контекст (new AppDbContext(options)), если у вас есть доступ к DbContextOptions.
Сначала регистрируем фабрику в Program.cs:
// Регистрирует фабрику как Singleton
// Также регистрирует AppDbContext как Scoped
builder.Services.AddDbContextFactory<AppDbContext>(
opts =>
{
opts.UseNpgsql(
builder.Configuration.GetConnectionString("db"));
});
Теперь перепишем конечную точку, используя фабрику контекстов.
Внутри метода мы запускаем отдельную задачу на каждый запрос.
В каждой задаче создаётся свой DbContext, выполняется запрос и сразу же освобождается.
using Microsoft.EntityFrameworkCore;
public class DashboardService(
IDbContextFactory<AppDbContext> ctxFactory)
{
public async Task<DashboardDto> GetDashboardAsync(int id)
{
var orders = GetOrdersAsync(id);
var logs = GetSystemLogsAsync();
var stats = GetUserStatsAsync(id);
await Task.WhenAll(orders, logs, stats);
return new DashboardDto(
await orders,
await logs,
await stats
);
}
private async Task<List<Order>> GetOrdersAsync(int id)
{
// Создаём отдельный контекст для операции
await using var ctx =
await ctxFactory.CreateDbContextAsync();
return await ctx.Orders
.AsNoTracking()
.Where(o => o.UserId == id)
.ToListAsync();
}
// остальные методы аналогичны
}
Ключевые моменты
1. Изоляция
Каждая задача получает свой DbContext.
Соответственно, у каждой задачи своё соединение с БД.
Никаких конфликтов.
2. Освобождение ресурсов
await using здесь критически важен.
Как только запрос завершился, контекст сразу освобождается, а соединение возвращается в пул.
При последовательном выполнении каждая операция ждёт завершения предыдущей.
При параллельном подходе все три запроса к БД стартуют и завершаются одновременно, что уменьшает общее время ответа.
Компромиссы и выводы
IDbContextFactory хорошо закрывает разрыв между unit-of-work-архитектурой EF Core и реальными требованиями к параллельной обработке.
Он позволяет выйти за рамки принципа «один запрос — один поток» без нарушения безопасности.
Но использовать этот подход нужно с головой:
1. Истощение пула соединений
Один HTTP-запрос теперь может одновременно занять 3 соединения с БД вместо одного.
При высокой нагрузке пул соединений можно выбить довольно быстро.
2. Накладные расходы
Если запросы очень быстрые (например, простой поиск по ID), накладные расходы на создание контекстов и задач могут сделать параллельный вариант медленнее последовательного.
Итог:
Если панель мониторинга работает медленно, не спешите сразу писать чистый SQL.
Посмотрите, не выполняются ли независимые запросы последовательно.
В таких случаях параллелизация через IDbContextFactory может дать заметный прирост.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16❤7😁1
Шокирующие бенчмарки производительности итераторов в C# / .NET.
Что лучше использовать в API для получения данных — итератор или материализованную коллекцию? В этом видео разбирают их реальные характеристики производительности, сравнивая итераторы в C# / .NET с обычными коллекциями на бенчмарках.
После этого видео стоит посмотреть продолжение:
https://youtube.com/watch?v=0s_VMhZSOwQ
Также можно идти параллельно по статье в блоге:
https://devleader.ca/2023/03/17/shocking-results-from-collection-and-iterator-benchmarks/
Исходники из видео здесь:
https://github.com/ncosentino/DevLeader/tree/master/AllAboutEnumerables/AllAboutEnumerables.BasicIteratorBenchmarks
Если хочется глубже разобраться в enumerable, итераторах и коллекциях, смотри плейлист:
https://youtube.com/watch?v=RR7Cq0iwNYo&list=PLzATctVhnsgjE3qOsbkPaC1NxXD605wOC
Само видео:
https://youtube.com/watch?v=G2-d7kZFlRA
👉 @KodBlog
Что лучше использовать в API для получения данных — итератор или материализованную коллекцию? В этом видео разбирают их реальные характеристики производительности, сравнивая итераторы в C# / .NET с обычными коллекциями на бенчмарках.
После этого видео стоит посмотреть продолжение:
https://youtube.com/watch?v=0s_VMhZSOwQ
Также можно идти параллельно по статье в блоге:
https://devleader.ca/2023/03/17/shocking-results-from-collection-and-iterator-benchmarks/
Исходники из видео здесь:
https://github.com/ncosentino/DevLeader/tree/master/AllAboutEnumerables/AllAboutEnumerables.BasicIteratorBenchmarks
Если хочется глубже разобраться в enumerable, итераторах и коллекциях, смотри плейлист:
https://youtube.com/watch?v=RR7Cq0iwNYo&list=PLzATctVhnsgjE3qOsbkPaC1NxXD605wOC
Само видео:
https://youtube.com/watch?v=G2-d7kZFlRA
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Давка кэша в C#: почему ConcurrentDictionary и MemoryCache не спасают
В статье разбирается проблема cache stampede и то, как она приводит к многократным тяжёлым запросам. Показаны бенчмарки для IO-bound и CPU-bound сценариев, есть сравнение с HybridCache и разбор подходов к защите кэша.
Показано, что GetOrAdd и GetOrCreateAsync не гарантируют единичный вызов тяжёлой операции. HybridCache умеет защищать только внутри одного процесса, но не между репликами. В конце — практические рекомендации, как выбирать решения и обходить проблему.
Предлагается добавлять случайный джиттер к TTL (в FusionCache это уже есть) и использовать паттерн Single Flight на уровне L2, чтобы обновление тяжёлых данных выполняла только одна реплика. Готовых библиотек под это автор не нашёл.
Читать подробнее: https://habr.com/ru/articles/977498/
👉 @KodBlog
В статье разбирается проблема cache stampede и то, как она приводит к многократным тяжёлым запросам. Показаны бенчмарки для IO-bound и CPU-bound сценариев, есть сравнение с HybridCache и разбор подходов к защите кэша.
Показано, что GetOrAdd и GetOrCreateAsync не гарантируют единичный вызов тяжёлой операции. HybridCache умеет защищать только внутри одного процесса, но не между репликами. В конце — практические рекомендации, как выбирать решения и обходить проблему.
Предлагается добавлять случайный джиттер к TTL (в FusionCache это уже есть) и использовать паттерн Single Flight на уровне L2, чтобы обновление тяжёлых данных выполняла только одна реплика. Готовых библиотек под это автор не нашёл.
Читать подробнее: https://habr.com/ru/articles/977498/
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1👍1
ToArrayAsync или ToListAsync в Entity Framework?
Очевидно, каждый из этих методов стоит использовать в своей ситуации. Но если разницы нет, что выбрать? Короткий ответ: ToListAsync.
Посмотрим на реализацию. ToListAsync выглядит так:
Источник
А вот как реализован ToArrayAsync:
Источник
Итого: ToArrayAsync сначала вызывает ToListAsync, а потом превращает полученный список в массив через ToArray(). В результате появляются лишние накладные расходы: создание списка, его наполнение, затем создание массива и копирование элементов.
Это не относится к ToHashSetAsync — он реализован по той же схеме, что и ToListAsync, без промежуточного списка.
👉 @KodBlog
Очевидно, каждый из этих методов стоит использовать в своей ситуации. Но если разницы нет, что выбрать? Короткий ответ: ToListAsync.
Посмотрим на реализацию. ToListAsync выглядит так:
public static async Task<List<TSource>>
ToListAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
{
var list = new List<TSource>();
await foreach (var element in
source
.AsAsyncEnumerable()
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
list.Add(element);
}
return list;
}
Источник
А вот как реализован ToArrayAsync:
public static async Task<TSource[]>
ToArrayAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
=> (
await source
.ToListAsync(cancellationToken)
.ConfigureAwait(false))
.ToArray();
Источник
Итого: ToArrayAsync сначала вызывает ToListAsync, а потом превращает полученный список в массив через ToArray(). В результате появляются лишние накладные расходы: создание списка, его наполнение, затем создание массива и копирование элементов.
Это не относится к ToHashSetAsync — он реализован по той же схеме, что и ToListAsync, без промежуточного списка.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤4
Как включать фичи без редеплоя приложения?
Используй feature flags
Feature flags в dotnet позволяют на лету включать или выключать функциональность во время работы приложения. Так проще контролировать поведение без нового релиза.
Это удобно для постепенных раскаток, A/B тестов и безопасного выкатывания новых фич.
Ключевые идеи:
• Feature Flags: переключатели, которые определяют, включена фича или нет.
• Feature Management: встроенная поддержка в .NET 8 для управления флагами через API feature management.
• Gradual Rollout: можно открыть фичу части пользователей и только потом включить всем.
• Configuration Sources: флаги можно хранить в разных источниках, например appsettings.json, Azure App Configuration или во внешних сервисах.
• Feature Filters: условия и правила, когда фича должна включаться (роль пользователя, окружение, кастомная логика и т.д.).
Пример:
Есть новая фича, и ты хочешь проверить, как пользователи на неё отреагируют.
Feature flags позволяют показать её только определённому проценту аудитории, например 10%.
Допустим, за день на сайт заходит 1000 человек. Тогда 100 из них увидят новую фичу.
Так можно аккуратно посмотреть реакцию на изменения и не рисковать всем трафиком сразу.
👉 @KodBlog
Используй feature flags
Feature flags в dotnet позволяют на лету включать или выключать функциональность во время работы приложения. Так проще контролировать поведение без нового релиза.
Это удобно для постепенных раскаток, A/B тестов и безопасного выкатывания новых фич.
Ключевые идеи:
• Feature Flags: переключатели, которые определяют, включена фича или нет.
• Feature Management: встроенная поддержка в .NET 8 для управления флагами через API feature management.
• Gradual Rollout: можно открыть фичу части пользователей и только потом включить всем.
• Configuration Sources: флаги можно хранить в разных источниках, например appsettings.json, Azure App Configuration или во внешних сервисах.
• Feature Filters: условия и правила, когда фича должна включаться (роль пользователя, окружение, кастомная логика и т.д.).
Пример:
Есть новая фича, и ты хочешь проверить, как пользователи на неё отреагируют.
Feature flags позволяют показать её только определённому проценту аудитории, например 10%.
Допустим, за день на сайт заходит 1000 человек. Тогда 100 из них увидят новую фичу.
Так можно аккуратно посмотреть реакцию на изменения и не рисковать всем трафиком сразу.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤3
This media is not supported in your browser
VIEW IN TELEGRAM
Gemini Code Wiki
Бесплатный инструмент, который генерирует интерактивную документацию по любому GitHub-репозиторию🤯
Что он умеет:
→ можно общаться с Gemini и разбираться в любом репо
→ визуализирует структуру кода
→ генерирует документацию из публичных репозиториев
👉 @KodBlog
Бесплатный инструмент, который генерирует интерактивную документацию по любому GitHub-репозиторию
Что он умеет:
→ можно общаться с Gemini и разбираться в любом репо
→ визуализирует структуру кода
→ генерирует документацию из публичных репозиториев
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤4😁2
Используем выражения коллекций для пользовательских типов
В C# 12 появились выражения коллекций — новый, более лаконичный синтаксис инициализации коллекций:
Этот синтаксис отлично работает со встроенными коллекциями. Но что делать со своими типами коллекций? Тут на сцену выходит атрибут CollectionBuilderAttribute. Он позволяет распространить этот современный синтаксис на пользовательские типы.
Пользовательские коллекции
Представим, что у вас есть собственный тип коллекции:
Раньше использовать выражения коллекций с таким типом было нельзя. Приходилось писать по-старому:
Или так (что по сути не сильно лучше):
Выглядит не особо элегантно.
CollectionBuilderAttribute закрывает этот пробел, подсказывая компилятору, как создавать вашу коллекцию из выражения коллекции:
После этого можно писать так:
Компилятор сам вызовет метод Create и передаст ему элементы.
Как это работает
Атрибут принимает два параметра:
1. Тип билдера — тип, в котором находится фабричный метод (typeof(MyCollectionBuilder)).
2. Имя метода — имя статического метода, создающего экземпляр ("Create").
Метод билдера должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типов, совпадающие с параметрами вашей коллекции.
Примечание: у коллекции также должен быть «итерационный контракт», то есть метод GetEnumerator(), возвращающий IEnumerator или IEnumerator<T>. Это можно сделать либо через реализацию IEnumerable / IEnumerable<T>, либо просто добавив метод GetEnumerator().
Когда все эти условия выполнены, компилятор сможет сгенерировать код создания вашей коллекции максимально эффективным образом.
👉 @KodBlog
В C# 12 появились выражения коллекций — новый, более лаконичный синтаксис инициализации коллекций:
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Этот синтаксис отлично работает со встроенными коллекциями. Но что делать со своими типами коллекций? Тут на сцену выходит атрибут CollectionBuilderAttribute. Он позволяет распространить этот современный синтаксис на пользовательские типы.
Пользовательские коллекции
Представим, что у вас есть собственный тип коллекции:
public class MyCollection<T>
{
private readonly List<T> _items;
public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];
public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();
// другие члены…
}
Раньше использовать выражения коллекций с таким типом было нельзя. Приходилось писать по-старому:
var myCol =
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });
Или так (что по сути не сильно лучше):
var myCol =
new MyCollection<int>([1, 2, 3, 4, 5]);
Выглядит не особо элегантно.
CollectionBuilderAttribute
CollectionBuilderAttribute закрывает этот пробел, подсказывая компилятору, как создавать вашу коллекцию из выражения коллекции:
[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
// …
}
public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}
После этого можно писать так:
MyCollection<int> myCol = [1, 2, 3, 4, 5];
Компилятор сам вызовет метод Create и передаст ему элементы.
Как это работает
Атрибут принимает два параметра:
1. Тип билдера — тип, в котором находится фабричный метод (typeof(MyCollectionBuilder)).
2. Имя метода — имя статического метода, создающего экземпляр ("Create").
Метод билдера должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типов, совпадающие с параметрами вашей коллекции.
Примечание: у коллекции также должен быть «итерационный контракт», то есть метод GetEnumerator(), возвращающий IEnumerator или IEnumerator<T>. Это можно сделать либо через реализацию IEnumerable / IEnumerable<T>, либо просто добавив метод GetEnumerator().
Когда все эти условия выполнены, компилятор сможет сгенерировать код создания вашей коллекции максимально эффективным образом.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍4❤2🍌1
PostgreSQL позволяет клонировать базу на 6 ГБ за 212 миллисекунд вместо 67 секунд. Вот как это работает.
Клонирование баз данных полезно в нескольких сценариях:
* тестирование миграций без трогания продакшн-данных
* поднятие свежих копий под каждый прогон тестов
* сброс sandbox-окружения между сессиями
* воспроизводимые снапшоты для отладки
Когда база весит несколько мегабайт, pg_dump вполне справляется. Но когда речь идёт о сотнях гигабайт, «просто сделать копию» превращается в серьёзный узкий момент.
В PostgreSQL система шаблонов была всегда. Каждый
В версии 15 появился параметр
В PostgreSQL 18 появилась опция
Всю магию здесь делает файловая система, создавая copy-on-write (CoW) клон файлов.
Когда вы обновляете строку, файловая система запускает copy-on-write только для затронутых страниц. Всё остальное остаётся общим. Клон на 6 ГБ изначально не занимает дополнительного места и растёт только по мере расхождения данных.
Важно помнить один момент: у исходной базы не должно быть активных подключений во время клонирования. Это ограничение PostgreSQL, а не файловой системы.
Довольно круто.
👉 @KodBlog
Клонирование баз данных полезно в нескольких сценариях:
* тестирование миграций без трогания продакшн-данных
* поднятие свежих копий под каждый прогон тестов
* сброс sandbox-окружения между сессиями
* воспроизводимые снапшоты для отладки
Когда база весит несколько мегабайт, pg_dump вполне справляется. Но когда речь идёт о сотнях гигабайт, «просто сделать копию» превращается в серьёзный узкий момент.
В PostgreSQL система шаблонов была всегда. Каждый
CREATE DATABASE тихо клонирует template1 под капотом, и вместо template1 можно использовать любую базу. В версии 15 появился параметр
STRATEGY, и по умолчанию включили WAL_LOG — поблочное копирование через журнал предзаписи (WAL). I/O становится ровнее, но для больших баз это медленнее.В PostgreSQL 18 появилась опция
file_copy_method = clone. На современных файловых системах вроде XFS, ZFS или APFS она использует операцию FICLONE. Вместо копирования байтов файловая система создаёт новые метаданные, которые указывают на те же физические блоки. Обе базы используют одно и то же хранилище, пока вы ничего не меняете.Всю магию здесь делает файловая система, создавая copy-on-write (CoW) клон файлов.
Когда вы обновляете строку, файловая система запускает copy-on-write только для затронутых страниц. Всё остальное остаётся общим. Клон на 6 ГБ изначально не занимает дополнительного места и растёт только по мере расхождения данных.
Важно помнить один момент: у исходной базы не должно быть активных подключений во время клонирования. Это ограничение PostgreSQL, а не файловой системы.
Довольно круто.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10🔥7🎉3👍1🍌1
Пример кода с FormattableString в C#
- Создание из списка с цветом для вывода
- Достаём нужные свойства через extension-методы
- Фильтрация в простом варианте
Читать подробнее
👉 @KodBlog
- Создание из списка с цветом для вывода
- Достаём нужные свойства через extension-методы
- Фильтрация в простом варианте
Читать подробнее
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👎4👍3🍌1
Вы всё ещё вызываете new Random() по каждому поводу? Пора перестать.
В сети часто повторяют: new Random() — это плохо.
Мол, он берёт сид из системных часов, и если создать несколько экземпляров подряд, то значения будут одинаковыми. Типа такой код:
выведет:
А вот этот вариант:
даст что-то вроде:
Откуда ноги растут?
В старом .NET Framework new Random() реально сидировался по DateTime.Now.Ticks, которые обновлялись примерно раз в 16 мс. Если создавать экземпляры чаще, сид совпадал, и результаты были одинаковыми. В .NET Core и дальше это уже не так. Можно прогнать код и убедиться.
А как насчёт потокобезопасности?
Наивный вариант, когда делают один общий генератор:
Выглядит норм, но потокобезопасным это не назовёшь. И проблемы могут всплыть не сразу.
Random при гонках в потоках часто возвращает 0, так что можно посмотреть масштаб проблемы:
В .NET 5 этот пример легко даёт 9000+ нулей, то есть почти всё упирается в проблемы потоков.
В .NET 8+ ситуация сильно лучше, но если задать сид вручную:
то проблемы возвращаются.
Что использовать?
Random.Shared создаёт свой экземпляр на поток (через [ThreadStatic]), поэтому для большинства кейсов на современных .NET это нормальный выбор. Если не нужен строго заданный сид — берите Random.Shared.
new Random() на новых версиях тоже обычно ок, но Shared безопаснее в многопоточке.
Про .NET Framework чуть позже.
👉 @KodBlog
В сети часто повторяют: new Random() — это плохо.
Мол, он берёт сид из системных часов, и если создать несколько экземпляров подряд, то значения будут одинаковыми. Типа такой код:
for (var i = 0; i < 5; i++)
{
var random = new Random();
Console.WriteLine(random.Next(1, 100));
}
выведет:
42 42 42 42 42
А вот этот вариант:
for (var i = 0; i < 5; i++)
Console.WriteLine(Random.Shared.Next(1, 100));
даст что-то вроде:
17 82 19 23 5
Откуда ноги растут?
В старом .NET Framework new Random() реально сидировался по DateTime.Now.Ticks, которые обновлялись примерно раз в 16 мс. Если создавать экземпляры чаще, сид совпадал, и результаты были одинаковыми. В .NET Core и дальше это уже не так. Можно прогнать код и убедиться.
А как насчёт потокобезопасности?
Наивный вариант, когда делают один общий генератор:
Random rng = new();
Parallel.For(0, 10, x =>
{
var value = rng.Next();
Console.WriteLine(value);
});
Выглядит норм, но потокобезопасным это не назовёшь. И проблемы могут всплыть не сразу.
Random при гонках в потоках часто возвращает 0, так что можно посмотреть масштаб проблемы:
Random rng = new();
Parallel.For(0, 10, x =>
{
var nums = new int[10_000];
for (int i = 0; i < nums.Length; ++i)
nums[i] = rng.Next();
Console.WriteLine($"{nums.Count(x => x == 0)} нулей");
});
В .NET 5 этот пример легко даёт 9000+ нулей, то есть почти всё упирается в проблемы потоков.
В .NET 8+ ситуация сильно лучше, но если задать сид вручную:
Random rng = new(123);
то проблемы возвращаются.
Что использовать?
Random.Shared создаёт свой экземпляр на поток (через [ThreadStatic]), поэтому для большинства кейсов на современных .NET это нормальный выбор. Если не нужен строго заданный сид — берите Random.Shared.
new Random() на новых версиях тоже обычно ок, но Shared безопаснее в многопоточке.
Про .NET Framework чуть позже.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤16
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Проблема с Random в .NET Framework
У Random в .NET Framework по сути две основные болячки:
При new Random() в .NET Framework сид берется от системных часов.
У системных часов ограниченная точность (разрешение).
Из-за этого при быстром, плотном создании new Random() можно легко получить два (или больше) инстанса Random с одинаковым seed. А значит, они выдадут одну и ту же последовательность чисел:
В результате часть чисел (а иногда и вообще все) могут совпасть, что нам обычно вообще не нужно.
Решение
Идея простая: держим один общий Random, который используется только чтобы раздавать seed’ы для остальных Random. Раз сиды разные, то и последовательности у локальных Random не будут коррелировать, и проблема с одинаковыми значениями уйдет.
В .NET Core / .NET это как раз поведение “из коробки”, поэтому баг проявляется именно в .NET Framework.
Ниже пример обертки ThreadSafeRandom. Она использует [ThreadStatic] и аккуратно обходит проблему инициализации в .NET Framework:
Эту потокобезопасную реализацию можно юзать на любых версиях фреймворка:
Если вы на .NET 6+, лучше использовать встроенный Random.Shared. Для более ранних версий подойдет ThreadSafeRandom выше. А если таргетитесь и в .NET 6+, и в .NET Framework, можно развести реализации через директивы #if под разные target framework.
👉 @KodBlog
У Random в .NET Framework по сути две основные болячки:
При new Random() в .NET Framework сид берется от системных часов.
У системных часов ограниченная точность (разрешение).
Из-за этого при быстром, плотном создании new Random() можно легко получить два (или больше) инстанса Random с одинаковым seed. А значит, они выдадут одну и ту же последовательность чисел:
// <TargetFramework>net48</TargetFramework>
Parallel.For(0, 10, x =>
{
Random rnd = new();
var value = rnd.Next();
Console.WriteLine(value);
});
В результате часть чисел (а иногда и вообще все) могут совпасть, что нам обычно вообще не нужно.
Решение
Идея простая: держим один общий Random, который используется только чтобы раздавать seed’ы для остальных Random. Раз сиды разные, то и последовательности у локальных Random не будут коррелировать, и проблема с одинаковыми значениями уйдет.
В .NET Core / .NET это как раз поведение “из коробки”, поэтому баг проявляется именно в .NET Framework.
Ниже пример обертки ThreadSafeRandom. Она использует [ThreadStatic] и аккуратно обходит проблему инициализации в .NET Framework:
internal static class ThreadSafeRandom
{
[ThreadStatic]
private static Random? _local;
private static readonly
Random Global = new Random();
private static Random Instance
{
get
{
if (_local is null)
{
int seed;
// избегаем конкурентного доступа
lock (Global)
{
seed = Global.Next();
}
_local = new Random(seed);
}
return _local;
}
}
public static int Next() => Instance.Next();
}
Эту потокобезопасную реализацию можно юзать на любых версиях фреймворка:
Parallel.For(0, 10, x =>
{
var value = ThreadSafeRandom.Next();
Console.WriteLine(value);
});
Если вы на .NET 6+, лучше использовать встроенный Random.Shared. Для более ранних версий подойдет ThreadSafeRandom выше. А если таргетитесь и в .NET 6+, и в .NET Framework, можно развести реализации через директивы #if под разные target framework.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍2
Оптимизация памяти в C# (и немного в Unity): эффективные методы и стратегии
Язык программирования C#, несмотря на то, что обеспечивает автоматическое управление памятью с помощью механизма сборки мусора (GC), требует от разработчиков специальных знаний и навыков для оптимизации работы с памятью
Итак, давайте рассмотрим различные стратегии и методы оптимизации памяти в C#, которые помогают создавать эффективные и быстрые приложения.
Читать статью...
👉 @KodBlog
Язык программирования C#, несмотря на то, что обеспечивает автоматическое управление памятью с помощью механизма сборки мусора (GC), требует от разработчиков специальных знаний и навыков для оптимизации работы с памятью
Итак, давайте рассмотрим различные стратегии и методы оптимизации памяти в C#, которые помогают создавать эффективные и быстрые приложения.
Читать статью...
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8👎7