Твои 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
Библиотеки, которые реально используют в Microsoft.
Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются до уровня Bing и Azure.
1. Microsoft.IO.RecyclableMemoryStream
Проблема:
Когда в API гоняется большой трафик или крутятся экспортные задания, обычный new MemoryStream() вызывает фрагментацию LOH (Large Object Heap) и частые остановки GC. В проде типа Bing это легко выливается в
OutOfMemoryException.
Решение:
RecyclableMemoryStream:
- переиспользует буферы через пул,
- снижает аллокации и не лезет в LOH,
- даёт нормальную диагностику по памяти.
Где уместно:
ASP.NET API, которые возвращают PDF, изображения, Excel,
- фоновые задачи, работающие с файлами,
- бэкенды под gRPC и SignalR.
До:
После:
Бенч:
2. LoggerMessage (генератор кода)
Проблема:
В сервисах с высоким трафиком даже такие безобидные логи:
создают проблемы:
- лишние аллокации из-за интерполяции строк,
- боксинг значимых типов,
- нагруженный GC и просадка пропускной способности.
На масштабе Azure это превращается в задержки и прожорливость по памяти.
Решение:
LoggerMessage из .NET 8 генерирует методы на этапе компиляции:
- без интерполяции и упаковки типов,
- со структурированным форматированием через Span<T>,
- с IL без лишних аллокаций,
в 5–10 раз быстрее обычного ILogger.LogInformation(...).
Где уместно:
- нагруженные веб-API,
- воркфлоу/ивент-хендлеры, где логов много,
- телеметрия и критичные hot-path’ы.
До (аллокации):
После (генерация):
Можно вынести такие методы в статические хелперы и шарить между сервисами.
👉 @KodBlog
Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются до уровня Bing и Azure.
1. Microsoft.IO.RecyclableMemoryStream
Проблема:
Когда в API гоняется большой трафик или крутятся экспортные задания, обычный new MemoryStream() вызывает фрагментацию LOH (Large Object Heap) и частые остановки GC. В проде типа Bing это легко выливается в
OutOfMemoryException.
Решение:
RecyclableMemoryStream:
- переиспользует буферы через пул,
- снижает аллокации и не лезет в LOH,
- даёт нормальную диагностику по памяти.
Где уместно:
ASP.NET API, которые возвращают PDF, изображения, Excel,
- фоновые задачи, работающие с файлами,
- бэкенды под gRPC и SignalR.
До:
using var ms = new MemoryStream();
После:
var mgr = new RecyclableMemoryStreamManager();
using var ms = mgr.GetStream();
Бенч:
Method | Memory | Gen 0 | Gen 2
---------------- | -------| ----- | -----
MemoryStream | 85 KB | 1 | 1
RecyclableMemory | 2 KB | 0 | 0
2. LoggerMessage (генератор кода)
Проблема:
В сервисах с высоким трафиком даже такие безобидные логи:
_logger.LogInformation($"Обработка заказа {orderId}");создают проблемы:
- лишние аллокации из-за интерполяции строк,
- боксинг значимых типов,
- нагруженный GC и просадка пропускной способности.
На масштабе Azure это превращается в задержки и прожорливость по памяти.
Решение:
LoggerMessage из .NET 8 генерирует методы на этапе компиляции:
- без интерполяции и упаковки типов,
- со структурированным форматированием через Span<T>,
- с IL без лишних аллокаций,
в 5–10 раз быстрее обычного ILogger.LogInformation(...).
Где уместно:
- нагруженные веб-API,
- воркфлоу/ивент-хендлеры, где логов много,
- телеметрия и критичные hot-path’ы.
До (аллокации):
_logger.LogInformation($"Обработка запроса {orderId}");После (генерация):
[LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "Обработка запроса {OrderId}")]
partial void LogProcessingOrder(int orderId);
// вызов
LogProcessingOrder(orderId);Можно вынести такие методы в статические хелперы и шарить между сервисами.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥22❤6👍3👎1🤣1
C# Portal | Программирование
Библиотеки, которые реально используют в Microsoft. Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются…
Ещё стоит упомянуть:
3. Microsoft.SemanticKernel
Проблема: Обычных запросов и ответов уже не хватает для продакшн-приложений на ИИ. Нужны память между диалогами, разбиение целей на шаги и доступ к API вроде Microsoft 365 Copilot. Реализовать всё это вручную в .NET — тяжело и долго.
Решение:
SemanticKernel — официальный SDK от Microsoft для сборки похожих на агентов ИИ-процессов. Он даёт:
* Вызов инструментов и плагинов (C# или OpenAPI);
* Оркестрацию цепочек запросов;
* Долговременную память между вызовами;
* Планировщик для разбиения задач на подзадачи.
Когда стоит брать:
* Корпоративные ИИ-ассистенты (CRM, генерация отчётов и т.п.);
* ИИ-процессы, которые ходят во внутренние API/функции;
* Многошаговые чат-боты с планированием и памятью.
До (одиночный запрос):
После (оркестрация через SemanticKernel):
Можно регистрировать плагины, которые вызывают API, делают авторизацию, исполняют SQL или дергают другие сервисы — и всё из .NET. Документация
4. System.Threading.Channels + IAsyncEnumerable
Проблема: BlockingCollection, ConcurrentQueue и кастомные очереди в паттерне «производитель/потребитель» часто упираются в блокировки, нестабильность и проблемы под нагрузкой. Особенно в телеметрии и потоковых пайплайнах.
Решение:
* Быстрый обмен сообщениями внутри процесса;
* Контроль пропускной способности через ограниченные каналы;
* Простую связку с IAsyncEnumerable<T>;
* Асинхронный стриминг без лишних выделений памяти.
Изначально жило внутри SignalR и gRPC, сейчас это нормальный способ собирать отказоустойчивые асинхронные конвейеры в .NET.
Когда использовать:
* Сбор телеметрии и обработка метрик;
* Асинхронные пайплайны логов/событий;
* Очередеподобное поведение без внешней брокерской инфраструктуры;
* Изоляция перегородками (Bulkhead) в фоновых сервисах.
До (BlockingCollection или Queue<T>):
После (каналы и async-потоки):
Отлично работает из коробки с await foreach и подходит для фоновых задач с I/O, стриминговых API и повторяющихся пайплайнов.
👉 @KodBlog
3. Microsoft.SemanticKernel
Проблема: Обычных запросов и ответов уже не хватает для продакшн-приложений на ИИ. Нужны память между диалогами, разбиение целей на шаги и доступ к API вроде Microsoft 365 Copilot. Реализовать всё это вручную в .NET — тяжело и долго.
Решение:
SemanticKernel — официальный SDK от Microsoft для сборки похожих на агентов ИИ-процессов. Он даёт:
* Вызов инструментов и плагинов (C# или OpenAPI);
* Оркестрацию цепочек запросов;
* Долговременную память между вызовами;
* Планировщик для разбиения задач на подзадачи.
Когда стоит брать:
* Корпоративные ИИ-ассистенты (CRM, генерация отчётов и т.п.);
* ИИ-процессы, которые ходят во внутренние API/функции;
* Многошаговые чат-боты с планированием и памятью.
До (одиночный запрос):
var result = await openAiClient
.GetCompletionAsync("Какая погода в Москве?");
После (оркестрация через SemanticKernel):
var kernel = Kernel.CreateBuilder().Build();
var prompt = kernel
.CreateFunctionFromPrompt("Какая погода в Москве?");
var result = await prompt.InvokeAsync();
Можно регистрировать плагины, которые вызывают API, делают авторизацию, исполняют SQL или дергают другие сервисы — и всё из .NET. Документация
4. System.Threading.Channels + IAsyncEnumerable
Проблема: BlockingCollection, ConcurrentQueue и кастомные очереди в паттерне «производитель/потребитель» часто упираются в блокировки, нестабильность и проблемы под нагрузкой. Особенно в телеметрии и потоковых пайплайнах.
Решение:
System.Threading.Channels даёт:* Быстрый обмен сообщениями внутри процесса;
* Контроль пропускной способности через ограниченные каналы;
* Простую связку с IAsyncEnumerable<T>;
* Асинхронный стриминг без лишних выделений памяти.
Изначально жило внутри SignalR и gRPC, сейчас это нормальный способ собирать отказоустойчивые асинхронные конвейеры в .NET.
Когда использовать:
* Сбор телеметрии и обработка метрик;
* Асинхронные пайплайны логов/событий;
* Очередеподобное поведение без внешней брокерской инфраструктуры;
* Изоляция перегородками (Bulkhead) в фоновых сервисах.
До (BlockingCollection или Queue<T>):
var queue = new Queue<int>();
lock (queue) { queue.Enqueue(42); }
int item;
lock (queue) { item = queue.Dequeue(); }
После (каналы и async-потоки):
var ch = Channel.CreateUnbounded<int>();
await ch.Writer.WriteAsync(42);
await foreach (var item in ch.Reader.ReadAllAsync())
Console.WriteLine(item);
Отлично работает из коробки с await foreach и подходит для фоновых задач с I/O, стриминговых API и повторяющихся пайплайнов.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍3
Может кому понадобится: проект генерирует архитектурные диаграммы из кода.
https://github.com/likec4/likec4/🤨
👉 @KodBlog
https://github.com/likec4/likec4/
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10🤣2❤1