Производительность платежной системы в проде была увеличена примерно в 15 раз — всего одной строкой кода в EF Core
Система обрабатывает массовые платежи, разбивая один запрос на множество аккаунтов получателей.
В тестах всё работало нормально, но при переходе в прод с реальными объёмами данных API стал тратить секунды на обработку одного запроса по нескольким тысячам счетов.
Каждый платежный запрос делает выборку из таблицы payment_accounts, чтобы определить счета, которые еще не были полностью оплачены.
Тест на 19 000 аккаунтов в SQL Server показал время обработки: 8.22 секунды.
Требование SLA (p99): 1 секунда.
API был примерно в 8 раз медленнее порога SLA.
Основная проблема оказалась в методе SaveChangesAsync.
EF Core умеет группировать несколько вставок, но база данных все равно выполняет их по отдельности.
В SQL Server EF Core использует оператор MERGE для массовых вставок, но подход ограничен лимитом SQL-параметров (2100 на батч). Если у сущностей много полей = производительность быстро падает.
После оптимизаций стало понятно, что штатными инструментами EF Core не удастся добиться нужной скорости.
Решением стала библиотека Entity Framework Extensions.
Она даёт простой, гибкий и быстрый способ для bulk-вставок, позволяя вставлять тысячи записей за один запрос к базе.
Методы BulkInsert и BulkInsertOptimized позволяют выполнить массовую вставку буквально одной строкой кода.
С их помощью время вставки удалось снизить до 521 мс, что укладывается в SLA.
👉 @KodBlog
Система обрабатывает массовые платежи, разбивая один запрос на множество аккаунтов получателей.
В тестах всё работало нормально, но при переходе в прод с реальными объёмами данных API стал тратить секунды на обработку одного запроса по нескольким тысячам счетов.
Каждый платежный запрос делает выборку из таблицы payment_accounts, чтобы определить счета, которые еще не были полностью оплачены.
Тест на 19 000 аккаунтов в SQL Server показал время обработки: 8.22 секунды.
Требование SLA (p99): 1 секунда.
API был примерно в 8 раз медленнее порога SLA.
Основная проблема оказалась в методе SaveChangesAsync.
EF Core умеет группировать несколько вставок, но база данных все равно выполняет их по отдельности.
В SQL Server EF Core использует оператор MERGE для массовых вставок, но подход ограничен лимитом SQL-параметров (2100 на батч). Если у сущностей много полей = производительность быстро падает.
После оптимизаций стало понятно, что штатными инструментами EF Core не удастся добиться нужной скорости.
Решением стала библиотека Entity Framework Extensions.
Она даёт простой, гибкий и быстрый способ для bulk-вставок, позволяя вставлять тысячи записей за один запрос к базе.
Методы BulkInsert и BulkInsertOptimized позволяют выполнить массовую вставку буквально одной строкой кода.
С их помощью время вставки удалось снизить до 521 мс, что укладывается в SLA.
Please open Telegram to view this post
VIEW IN TELEGRAM
👌10👍7❤3🔥1
AsNoTracking() вроде ускоряет выполнение, но есть тонкий момент, о котором часто забывают, а именно о том , что он может создавать дубликаты сущностей в памяти.
Почему так происходит? Потому что без change tracker EF Core не делает identity resolution - он не проверяет, была ли эта сущность уже загружена ранее. В итоге можно получить несколько объектов, которые на самом деле представляют одну и ту же запись в базе.
Пример:
Если у нескольких заказов один и тот же пользователь, то Customer будет склонирован под каждый заказ. То есть:
• больше памяти
• сравнение сущностей по ссылке становится бесполезным
• нарушается связность графа объектов
Решение — AsNoTrackingWithIdentityResolution()
Этот метод даёт баланс:
• нет change tracking (то есть быстрый чтение-only режим)
• есть identity resolution (одна сущность — один объект)
Пример:
Теперь все заказы, у которых один и тот же Customer, будут ссылаться ровно на один объект пользователя.
Как это работает внутри:
• запрос выполняется без отслеживания
• EF Core создаёт временную identity map
• сущности с одинаковым ключом получат одинаковый объект
• карта удаляется после завершения запроса
То есть tracking нет, но дубликатов тоже нет.
Если это поведение нужно часто, его можно включить по умолчанию:
Когда использовать:
1. Отслеживание по умолчанию:
- необходимо обновлять, удалять или отслеживать изменения сущностей;
- небольшие наборы результатов, где затраты на отслеживание незначительны;
- необходимо автоматическое обнаружение изменений.
2. AsNoTracking:
- операции только чтения;
- нет свойств навигации или связанных сущностей;
- максимальная производительность критически важна;
- результаты не содержат дублирующихся сущностей.
3. AsNoTrackingWithIdentityResolution:
- операции только чтения с include/join;
- требуется ссылочная целостность в графе объектов;
- запросы, возвращающие одну сущность несколько раз;
- требуется сравнивать сущности по ссылке.
👉 @KodBlog
Почему так происходит? Потому что без change tracker EF Core не делает identity resolution - он не проверяет, была ли эта сущность уже загружена ранее. В итоге можно получить несколько объектов, которые на самом деле представляют одну и ту же запись в базе.
Пример:
var orders = await context.Orders
.AsNoTracking()
.Include(o => o.Customer)
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
.ToListAsync();
Если у нескольких заказов один и тот же пользователь, то Customer будет склонирован под каждый заказ. То есть:
• больше памяти
• сравнение сущностей по ссылке становится бесполезным
• нарушается связность графа объектов
Решение — AsNoTrackingWithIdentityResolution()
Этот метод даёт баланс:
• нет change tracking (то есть быстрый чтение-only режим)
• есть identity resolution (одна сущность — один объект)
Пример:
var orders = await context.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Customer)
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
.ToListAsync();
Теперь все заказы, у которых один и тот же Customer, будут ссылаться ровно на один объект пользователя.
Как это работает внутри:
• запрос выполняется без отслеживания
• EF Core создаёт временную identity map
• сущности с одинаковым ключом получат одинаковый объект
• карта удаляется после завершения запроса
То есть tracking нет, но дубликатов тоже нет.
Если это поведение нужно часто, его можно включить по умолчанию:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTrackingWithIdentityResolution);
}
Когда использовать:
1. Отслеживание по умолчанию:
- необходимо обновлять, удалять или отслеживать изменения сущностей;
- небольшие наборы результатов, где затраты на отслеживание незначительны;
- необходимо автоматическое обнаружение изменений.
2. AsNoTracking:
- операции только чтения;
- нет свойств навигации или связанных сущностей;
- максимальная производительность критически важна;
- результаты не содержат дублирующихся сущностей.
3. AsNoTrackingWithIdentityResolution:
- операции только чтения с include/join;
- требуется ссылочная целостность в графе объектов;
- запросы, возвращающие одну сущность несколько раз;
- требуется сравнивать сущности по ссылке.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Visual Studio 2026 официально вышла.
Раньше обновление Visual Studio автоматически тянуло за собой обновление .NET и C++ toolchain. Один пакет значит один риск. Хотел новые фичи IDE? Заодно получай новые компиляторы, нужные они тебе или нет.
В VS 2026 это разделили. IDE обновляется отдельно. Новые фичи прилетают раз в месяц. Build tools остаются в нужной версии.
Производительность подтянули по всем направлениям. Количество UI фризов уменьшилось на 50%. Запуск под F5 стал быстрее на 30% вместе с .NET 10. Большие решения загружаются ощутимо быстрее, а не только формально по цифрам.
Code coverage больше не только в Enterprise.
Теперь он доступен во всех версиях. Разработчики на Community и Professional могут анализировать покрытие тестами без апгрейда лицензии.
Интеграция с AI стала глубже. Copilot теперь понимает внешние символы в C#, а не только твой проект. Он может подтягивать контекст по URL. Debugger агент умеет автоматически чинить упавшие unit-тесты: предполагает причину, правит, проверяет и повторяет цикл, пока тест не пройдет.
Обратная совместимость полная. Проекты из VS 2022 открываются напрямую. Все 4000+ расширений работают без правок. Никакой миграции.
Fluent UI — самое заметное изменение, но далеко не главное. Главное то, что IDE стала быстрее, стабильнее и обновляется без принудительных обновлений compiler toolchain.
Что попробовать в первую очередь:
- Открой свою самую тяжелую solution и почувствуй разницу
- Запусти code coverage в Community/Professional
- Дай debugger агенту починить упавший тест
Уже доступно. Проекты и расширения из VS 2022 переносятся автоматически.
👉 @KodBlog
Раньше обновление Visual Studio автоматически тянуло за собой обновление .NET и C++ toolchain. Один пакет значит один риск. Хотел новые фичи IDE? Заодно получай новые компиляторы, нужные они тебе или нет.
В VS 2026 это разделили. IDE обновляется отдельно. Новые фичи прилетают раз в месяц. Build tools остаются в нужной версии.
Производительность подтянули по всем направлениям. Количество UI фризов уменьшилось на 50%. Запуск под F5 стал быстрее на 30% вместе с .NET 10. Большие решения загружаются ощутимо быстрее, а не только формально по цифрам.
Code coverage больше не только в Enterprise.
Теперь он доступен во всех версиях. Разработчики на Community и Professional могут анализировать покрытие тестами без апгрейда лицензии.
Интеграция с AI стала глубже. Copilot теперь понимает внешние символы в C#, а не только твой проект. Он может подтягивать контекст по URL. Debugger агент умеет автоматически чинить упавшие unit-тесты: предполагает причину, правит, проверяет и повторяет цикл, пока тест не пройдет.
Обратная совместимость полная. Проекты из VS 2022 открываются напрямую. Все 4000+ расширений работают без правок. Никакой миграции.
Fluent UI — самое заметное изменение, но далеко не главное. Главное то, что IDE стала быстрее, стабильнее и обновляется без принудительных обновлений compiler toolchain.
Что попробовать в первую очередь:
- Открой свою самую тяжелую solution и почувствуй разницу
- Запусти code coverage в Community/Professional
- Дай debugger агенту починить упавший тест
Уже доступно. Проекты и расширения из VS 2022 переносятся автоматически.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤24👍4👎2
Поддержка Server-Sent Events (SSE)
В
Server-Sent Events это механизм push-уведомлений, где сервер может отправлять поток событий клиенту по одному HTTP-соединению. В .NET каждое сообщение представлено как SseItem<T>, где может быть тип события, его ID и payload типа T.
В TypedResults появилась новая статическая функция ServerSentEvents, которая возвращает результат типа ServerSentEvents. Первый аргумент это IAsyncEnumerable<SseItem<T>> — поток событий, который будет отправляться клиенту.
Ниже пример, как с помощью TypedResults.ServerSentEvents стримить JSON-события с данными пульса:
👉 @KodBlog
В
ASP.NET Core теперь можно возвращать результаты в формате ServerSentEvents через API TypedResults.ServerSentEvents. Эта фича работает как в Minimal APIs, так и в контроллерах.Server-Sent Events это механизм push-уведомлений, где сервер может отправлять поток событий клиенту по одному HTTP-соединению. В .NET каждое сообщение представлено как SseItem<T>, где может быть тип события, его ID и payload типа T.
В TypedResults появилась новая статическая функция ServerSentEvents, которая возвращает результат типа ServerSentEvents. Первый аргумент это IAsyncEnumerable<SseItem<T>> — поток событий, который будет отправляться клиенту.
Ниже пример, как с помощью TypedResults.ServerSentEvents стримить JSON-события с данными пульса:
app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var heartRate = Random.Shared.Next(60, 100);
yield return HeartRateRecord.Create(heartRate);
await Task.Delay(2000, cancellationToken);
}
}
return TypedResults.ServerSentEvents(
GetHeartRate(cancellationToken),
eventType: "heartRate"
);
});Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8👏2🕊2❤1
Хватит выкатывать релиз каждый раз, когда появляется новый фильтр.
Пусть фильтры обновляются сами.
Что если твой LINQ Where принимал бы правило во время выполнения и при этом оставался нормальным SQL-транслируемым запросом?
Хардкодить фильтры кажется нормальным… пока продукт не приходит с очередным запросом: "добавьте ещё одно поле".
И вот уже вокруг if-джунгли, дублированные запросы и новый деплой ради мелкой правки. Динамические предикаты меняют всё → правила живут в конфиге/БД/UI, а код остаётся компактным и стабильным.
Где это реально помогает:
• Админки и отчёты → пользователь комбинирует поля/условия без твоих доп. веток.
• Поисковые API: query-string/JSON → динамический предикат, без плясок с expression trees.
• Multi-tenant/white-label → у каждого клиента свои правила, ты просто загружаешь и применяешь.
• Сегменты пользователей → маркетинг собирает аудитории вроде
"Активен в DACH И (последние 90 дней ИЛИ ≥ 500€) И подписан на рассылку" - без релиза.
Зачем оно нужно:
• Меньше условных веток → чище код и проще тестирование.
• Гибкость на рантайме → быстрее итерации, меньше релизов.
• Всё ещё IQueryable → EF прогоняет фильтры в SQL, а не в память.
Полезные практики:
• Ввести whitelist допустимых полей/операторов (безопасность и предсказуемость).
• Валидировать правила до сборки предиката.
• Оставлять всё на IQueryable до конца (пускай EF делает работу).
• Унифицировать формат дат и параметров, чтобы не ловить культуру-зависимые баги.
• Добавить snapshot-тесты для сохранённых фильтров/сегментов.
Когда это не нужно = если у вас всего 2–3 фиксированных фильтра, которые редко меняются, обычный LINQ проще и надёжнее.
Если у тебя когда-то было ощущение "я пишу фичи, а не переписываю запросы", значит этот подход тебе зайдёт.
Полный гайд и примеры
👉 @KodBlog
Пусть фильтры обновляются сами.
Что если твой LINQ Where принимал бы правило во время выполнения и при этом оставался нормальным SQL-транслируемым запросом?
Хардкодить фильтры кажется нормальным… пока продукт не приходит с очередным запросом: "добавьте ещё одно поле".
И вот уже вокруг if-джунгли, дублированные запросы и новый деплой ради мелкой правки. Динамические предикаты меняют всё → правила живут в конфиге/БД/UI, а код остаётся компактным и стабильным.
Где это реально помогает:
• Админки и отчёты → пользователь комбинирует поля/условия без твоих доп. веток.
• Поисковые API: query-string/JSON → динамический предикат, без плясок с expression trees.
• Multi-tenant/white-label → у каждого клиента свои правила, ты просто загружаешь и применяешь.
• Сегменты пользователей → маркетинг собирает аудитории вроде
"Активен в DACH И (последние 90 дней ИЛИ ≥ 500€) И подписан на рассылку" - без релиза.
Зачем оно нужно:
• Меньше условных веток → чище код и проще тестирование.
• Гибкость на рантайме → быстрее итерации, меньше релизов.
• Всё ещё IQueryable → EF прогоняет фильтры в SQL, а не в память.
Полезные практики:
• Ввести whitelist допустимых полей/операторов (безопасность и предсказуемость).
• Валидировать правила до сборки предиката.
• Оставлять всё на IQueryable до конца (пускай EF делает работу).
• Унифицировать формат дат и параметров, чтобы не ловить культуру-зависимые баги.
• Добавить snapshot-тесты для сохранённых фильтров/сегментов.
Когда это не нужно = если у вас всего 2–3 фиксированных фильтра, которые редко меняются, обычный LINQ проще и надёжнее.
Если у тебя когда-то было ощущение "я пишу фичи, а не переписываю запросы", значит этот подход тебе зайдёт.
Полный гайд и примеры
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11🥴7👍2🔥1
Использование Git Conditional Includes для нескольких конфигураций
Когда работаешь с несколькими репозиториями Git, часто нужны разные настройки под разные контексты. Например, можно использовать личную почту для open-source проектов и рабочую почту для корпоративных репозиториев. Да, можно настроить Git глобально или в каждом репозитории вручную, но со временем это становится муторно и легко ошибиться.
Функция Git conditional includes решает эту проблему = она автоматически применяет нужную конфигурацию в зависимости от условий вроде пути репозитория, URL remotes или имени ветки. В итоге нужные настройки подтягиваются сами - без ручных переключений.
Вот пример, который подключает отдельную конфигурацию, если работаешь с личными репозиториями GitHub:
Git проверяет remotes и если URL совпадает с шаблоном, он автоматически подгружает настройки из .gitconfig-github-personal, где, например, могут храниться твоя личная почта и ключ подписи.
Понимаем, как работают Git Conditional Includes
Git поддерживает несколько типов условий для conditional includes. Каждый подходит под свой сценарий.
gitdir - совпадение по пути репозитория
Условие gitdir срабатывает, если путь к репозиторию совпадает с заданным. На Unix-системах проверка чувствительна к регистру.
Так настройки .gitconfig-personal применяются ко всем репозиториям под ~/personal/, а .gitconfig-work к тем, что в ~/work/.
gitdir/i - совпадение по пути без учета регистра
То же самое, что gitdir, но нечувствительное к регистру. Полезно на Windows или когда нужен более гибкий матчинг.
onbranch - совпадение по текущей ветке
Это условие подключает конфигурацию в зависимости от того, какая ветка сейчас checkout -нута.
Удобно, когда нужно автоматически переключаться между продовыми и девелоперскими настройками.
hasconfig - совпадение по существующей настройке Git
Это условие проверяет, существует ли конкретная настройка и подходит ли под шаблон. Особенно полезно, чтобы матчить remote-URL.
Этот подход позволяет подключать разные конфиги в зависимости от того, где хостится репозиторий, независимо от его локального пути.
👉 @KodBlog
Когда работаешь с несколькими репозиториями Git, часто нужны разные настройки под разные контексты. Например, можно использовать личную почту для open-source проектов и рабочую почту для корпоративных репозиториев. Да, можно настроить Git глобально или в каждом репозитории вручную, но со временем это становится муторно и легко ошибиться.
Функция Git conditional includes решает эту проблему = она автоматически применяет нужную конфигурацию в зависимости от условий вроде пути репозитория, URL remotes или имени ветки. В итоге нужные настройки подтягиваются сами - без ручных переключений.
Вот пример, который подключает отдельную конфигурацию, если работаешь с личными репозиториями GitHub:
[includeIf "hasconfig:remote.*.url:https://github.com/meziantou/*"]
path = .gitconfig-github-personal
Git проверяет remotes и если URL совпадает с шаблоном, он автоматически подгружает настройки из .gitconfig-github-personal, где, например, могут храниться твоя личная почта и ключ подписи.
Понимаем, как работают Git Conditional Includes
Git поддерживает несколько типов условий для conditional includes. Каждый подходит под свой сценарий.
gitdir - совпадение по пути репозитория
Условие gitdir срабатывает, если путь к репозиторию совпадает с заданным. На Unix-системах проверка чувствительна к регистру.
[includeIf "gitdir:~/personal/"]
path = .gitconfig-personal
[includeIf "gitdir:~/work/"]
path = .gitconfig-work
Так настройки .gitconfig-personal применяются ко всем репозиториям под ~/personal/, а .gitconfig-work к тем, что в ~/work/.
gitdir/i - совпадение по пути без учета регистра
То же самое, что gitdir, но нечувствительное к регистру. Полезно на Windows или когда нужен более гибкий матчинг.
[includeIf "gitdir/i:c:/projects/company/"]
path = .gitconfig-company
onbranch - совпадение по текущей ветке
Это условие подключает конфигурацию в зависимости от того, какая ветка сейчас checkout -нута.
[includeIf "onbranch:main"]
path = .gitconfig-production
[includeIf "onbranch:feature/**"]
path = .gitconfig-development
Удобно, когда нужно автоматически переключаться между продовыми и девелоперскими настройками.
hasconfig - совпадение по существующей настройке Git
Это условие проверяет, существует ли конкретная настройка и подходит ли под шаблон. Особенно полезно, чтобы матчить remote-URL.
[includeIf "hasconfig:remote.*.url:https://github.com/meziantou/*"]
path = .gitconfig-github-personal
[includeIf "hasconfig:remote.*.url:[email protected]:*/**"]
path = .gitconfig-company
Этот подход позволяет подключать разные конфиги в зависимости от того, где хостится репозиторий, независимо от его локального пути.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤🔥2
API Gateway и Load Balancer часто путают новички, но это база для любого бэкенд разработчика
API Gateway работает как входная точка
• это единая точка, куда стучатся клиенты, он берет на себя всю первичную обработку запросов
• умный слой, который управляет авторизацией и аутентификацией (AuthN), rate limiting, логированием, мониторингом запросов и маршрутизацией
• идеально подходит для микросервисов, потому что централизует управление API во всей системе
• работает на уровне приложения L7
Load Balancer работает как распределитель трафика
• его задача распределять входящие запросы между группой одинаковых серверов
• нужен чтобы повысить пропускную способность, уменьшить задержки и не допускать перегрузки одного сервера
• подходит для систем с высокой нагрузкой, где важна отказоустойчивость и стабильное распределение трафика
• может работать как на уровне приложения L7, так и на транспортном уровне L4
API Gateway управляет, валидирует и контролирует API запросы (про сложность)
Load Balancer равномерно распределяет трафик и оптимизирует нагрузку (про производительность)
👉 @KodBlog
API Gateway работает как входная точка
• это единая точка, куда стучатся клиенты, он берет на себя всю первичную обработку запросов
• умный слой, который управляет авторизацией и аутентификацией (AuthN), rate limiting, логированием, мониторингом запросов и маршрутизацией
• идеально подходит для микросервисов, потому что централизует управление API во всей системе
• работает на уровне приложения L7
Load Balancer работает как распределитель трафика
• его задача распределять входящие запросы между группой одинаковых серверов
• нужен чтобы повысить пропускную способность, уменьшить задержки и не допускать перегрузки одного сервера
• подходит для систем с высокой нагрузкой, где важна отказоустойчивость и стабильное распределение трафика
• может работать как на уровне приложения L7, так и на транспортном уровне L4
API Gateway управляет, валидирует и контролирует API запросы (про сложность)
Load Balancer равномерно распределяет трафик и оптимизирует нагрузку (про производительность)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤2😁1
C# 14: Extension Members и как они помогают polyfill-библиотекам
C# давно поддерживает extension-методы, которые позволяют добавлять новые методы к существующим типам, не трогая исходный код. Но раньше это касалось только инстанс-методов. В C# 14 появились extension members, и область расширения заметно выросла. Теперь можно добавлять extension-свойства и extension-статические методы к существующим типам. Такая фича особенно полезна для polyfill-библиотек, которые позволяют использовать новые API в старых версиях .NET.
Polyfill-библиотеки помогают писать код с использованием современных API, но при этом таргетировать разные версии .NET. В предыдущем посте про polyfills я объяснял, что смысл таких библиотек заключается в том, чтобы предоставить реализацию новых API для старых фреймворков и уменьшить количество
До выхода C# 14 polyfill-библиотеки могли добавлять только инстанс-методы через extension methods. Это означало, что статические методы и свойства из новых версий .NET нормально не покрывались polyfill'ами. C# 14 снимает это ограничение и позволяет polyfill-библиотекам выглядеть куда полнее и нативнее.
Хороший пример это статический метод ArgumentNullException.ThrowIfNull, который появился в .NET 6. Он дает удобный способ проверить аргументы на null:
До C# 14 polyfill-библиотеки не могли добавить ThrowIfNull как статический метод ArgumentNullException, потому что extension методы работали только с инстансами. В C# 14 можно определить extension static method, и polyfill-библиотеки могут предоставить идентичный API даже для старых версий .NET. Теперь можно использовать ArgumentNullException.ThrowIfNull независимо от target framework, и код будет выглядеть единообразно и чище.
Конечно, можно писать свои extension static-методы и свойства, но проще воспользоваться уже поддерживаемой polyfill-библиотекой.
Пакет Meziantou.Polyfill (GitHub) может покрывать более 350 типов, методов и свойств. Он использует возможности C# 14, чтобы дать максимально полное polyfill-решение. Пакет основан на source generators, которые автоматически добавляют только те polyfills, которые реально нужны под ваш target framework.
Установка:
После установки можно использовать новые API вроде ArgumentNullException.ThrowIfNull даже при таргете на старые платформы, вроде .NET Standard 2.0 или .NET Framework:
Source generator автоматически определяет target framework, версию C# и опции компиляции, и генерирует extension members только тогда, когда это возможно и реально требуется. Если вы не используете C# 14 или ваш target framework уже содержит нужный API, polyfill просто не будет создан.
👉 @KodBlog
C# давно поддерживает extension-методы, которые позволяют добавлять новые методы к существующим типам, не трогая исходный код. Но раньше это касалось только инстанс-методов. В C# 14 появились extension members, и область расширения заметно выросла. Теперь можно добавлять extension-свойства и extension-статические методы к существующим типам. Такая фича особенно полезна для polyfill-библиотек, которые позволяют использовать новые API в старых версиях .NET.
Polyfill-библиотеки помогают писать код с использованием современных API, но при этом таргетировать разные версии .NET. В предыдущем посте про polyfills я объяснял, что смысл таких библиотек заключается в том, чтобы предоставить реализацию новых API для старых фреймворков и уменьшить количество
#if в проекте.До выхода C# 14 polyfill-библиотеки могли добавлять только инстанс-методы через extension methods. Это означало, что статические методы и свойства из новых версий .NET нормально не покрывались polyfill'ами. C# 14 снимает это ограничение и позволяет polyfill-библиотекам выглядеть куда полнее и нативнее.
Хороший пример это статический метод ArgumentNullException.ThrowIfNull, который появился в .NET 6. Он дает удобный способ проверить аргументы на null:
public void ProcessData(string data)
{
ArgumentNullException.ThrowIfNull(data);
// Process the data...
}
До C# 14 polyfill-библиотеки не могли добавить ThrowIfNull как статический метод ArgumentNullException, потому что extension методы работали только с инстансами. В C# 14 можно определить extension static method, и polyfill-библиотеки могут предоставить идентичный API даже для старых версий .NET. Теперь можно использовать ArgumentNullException.ThrowIfNull независимо от target framework, и код будет выглядеть единообразно и чище.
static class PolyfillExtensions
{
extension(ArgumentNullException)
{
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument is null)
throw new ArgumentNullException(paramName);
}
}
}
Конечно, можно писать свои extension static-методы и свойства, но проще воспользоваться уже поддерживаемой polyfill-библиотекой.
Пакет Meziantou.Polyfill (GitHub) может покрывать более 350 типов, методов и свойств. Он использует возможности C# 14, чтобы дать максимально полное polyfill-решение. Пакет основан на source generators, которые автоматически добавляют только те polyfills, которые реально нужны под ваш target framework.
Установка:
dotnet add package Meziantou.PolyfillПосле установки можно использовать новые API вроде ArgumentNullException.ThrowIfNull даже при таргете на старые платформы, вроде .NET Standard 2.0 или .NET Framework:
public class UserService
{
public void CreateUser(string username, string email)
{
// Работает в .NET Standard 2.0 благодаря extension static methods
ArgumentNullException.ThrowIfNull(username);
ArgumentNullException.ThrowIfNull(email);
// Create user logic...
}
}
Source generator автоматически определяет target framework, версию C# и опции компиляции, и генерирует extension members только тогда, когда это возможно и реально требуется. Если вы не используете C# 14 или ваш target framework уже содержит нужный API, polyfill просто не будет создан.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤3🔥3
На Хабре разобрали YARP — это быстрый reverse proxy на .NET, и его можно удобно конфигурировать прямо в
Под капотом есть всё, что нужно для продовой нагрузки:
• поддержка HTTP/2 и gRPC
• трансформации путей и заголовков
• балансировка трафика (RoundRobin, PowerOfTwoChoices и другие алгоритмы)
• health-checks и session affinity
• TLS-терминация
В статье есть разбор, примеры конфигурации и демо от OTUS
👉 @KodBlog
ASP.NET Core через appsettings.json.Под капотом есть всё, что нужно для продовой нагрузки:
• поддержка HTTP/2 и gRPC
• трансформации путей и заголовков
• балансировка трафика (RoundRobin, PowerOfTwoChoices и другие алгоритмы)
• health-checks и session affinity
• TLS-терминация
В статье есть разбор, примеры конфигурации и демо от OTUS
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤4🔥2
C# expression-bodied члены просто топ.
А теперь к делу. Вот способ использовать enum для сортировки строк, которые сами по себе ничего не значат.
Бывало, что нужно отсортировать данные, пришедшие откуда-то извне, например из веб-API, где нет нормального поля для сортировки? Enum может стать небольшим хаком и избавить от магических строк.
👉 @KodBlog
А теперь к делу. Вот способ использовать enum для сортировки строк, которые сами по себе ничего не значат.
Бывало, что нужно отсортировать данные, пришедшие откуда-то извне, например из веб-API, где нет нормального поля для сортировки? Enum может стать небольшим хаком и избавить от магических строк.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8
В Visual Studio 2026 появилась возможность отключать отображение символов под файлами C# и C++ в Solution Explorer, и эта функция скоро станет доступна.
👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14😁6👎2❤1👏1
Оптимизация производительности создания GUID в .NET приложениях
Во многих .NET проектах GUID используют как идентификаторы для активностей, interop-сценариев, ключей в базах данных и многого другого. Часто можно встретить код вида new Guid("01234567-8901-2345-6789-012345678901"). Такой способ читаемый и простой, но у него есть скрытая цена = он может замедлять старт приложения.
Создание GUID из строки требует парсинга строкового представления, а это уже дополнительные операции. Плюс .NET поддерживает несколько форматов строковых GUID (с фигурными скобками, без, с дефисами и т.д.), из-за чего логика парсинга тоже усложняется.
Если GUID зашит в коде, эту нагрузку можно убрать, используя конструктор Guid, который принимает числовые параметры.
Например, строковое создание GUID:
Можно заменить на:
Так GUID собирается напрямую из числовых компонентов, без парсинга строки. Минус в том, что читаемость страдает, и вручную конвертировать строку в numeric-формат легко ошибиться. Плюс такой код сложнее искать в проекте. Решением будет оставлять строковое значение как комментарий.
Автоматическое обнаружение с Meziantou.Analyzer
Чтобы находить такие места автоматически, Meziantou.Analyzer содержит правило MA0176, которое ищет создание GUID из строковых литералов и предлагает использовать numeric-конструктор.
Устанавливается через .NET CLI или добавлением в csproj.
CLI:
Или в .csproj:
MA0176 ловит такие случаи и предлагает оптимизацию:
И автоматически заменяет на:
// Оптимизированный вариант
new Guid(0x01234567, 0x8901, 0x2345, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01) /* 01234567-8901-2345-6789-012345678901 */;
Бенчмарк
Использовался BenchmarkDotNet для сравнения:
Разница огромная (×1000 в пользу числового конструктора), но в большинстве приложений создаётся мало фиксированных GUID, поэтому влияние на общую производительность минимальное. Но при старте приложения, а особенно в сценариях вроде Azure Functions или AWS Lambda, где важны cold start задержки, эта оптимизация может помочь.
👉 @KodBlog
Во многих .NET проектах GUID используют как идентификаторы для активностей, interop-сценариев, ключей в базах данных и многого другого. Часто можно встретить код вида new Guid("01234567-8901-2345-6789-012345678901"). Такой способ читаемый и простой, но у него есть скрытая цена = он может замедлять старт приложения.
Важно отметить, что это микрооптимизация, которая в основном полезна в случаях, когда GUID создаются во время запуска приложения. Для обычного кода разница практически незаметна (смотри бенчмарк ниже).
Создание GUID из строки требует парсинга строкового представления, а это уже дополнительные операции. Плюс .NET поддерживает несколько форматов строковых GUID (с фигурными скобками, без, с дефисами и т.д.), из-за чего логика парсинга тоже усложняется.
Если GUID зашит в коде, эту нагрузку можно убрать, используя конструктор Guid, который принимает числовые параметры.
Например, строковое создание GUID:
var guid = new Guid("01234567-8901-2345-6789-012345678901");Можно заменить на:
var guid = new Guid(0x01234567, 0x8901, 0x2345, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01);
Так GUID собирается напрямую из числовых компонентов, без парсинга строки. Минус в том, что читаемость страдает, и вручную конвертировать строку в numeric-формат легко ошибиться. Плюс такой код сложнее искать в проекте. Решением будет оставлять строковое значение как комментарий.
Автоматическое обнаружение с Meziantou.Analyzer
Чтобы находить такие места автоматически, Meziantou.Analyzer содержит правило MA0176, которое ищет создание GUID из строковых литералов и предлагает использовать numeric-конструктор.
Устанавливается через .NET CLI или добавлением в csproj.
CLI:
dotnet add package Meziantou.AnalyzerИли в .csproj:
<PackageReference Include="Meziantou.Analyzer" Version="2.0.224">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
MA0176 ловит такие случаи и предлагает оптимизацию:
// Строковое создание (будет замечено анализатором)
_ = new Guid("01234567-8901-2345-6789-012345678901");
_ = Guid.Parse("01234567-8901-2345-6789-012345678901");
И автоматически заменяет на:
// Оптимизированный вариант
new Guid(0x01234567, 0x8901, 0x2345, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01) /* 01234567-8901-2345-6789-012345678901 */;
Бенчмарк
Использовался BenchmarkDotNet для сравнения:
public class NewGuid
{
[Benchmark(Baseline = true)]
public Guid GuidParse() => Guid.Parse("01234567-8901-2345-6789-012345678901");
[Benchmark]
public Guid NewGuidString() => new Guid("01234567-8901-2345-6789-012345678901");
[Benchmark]
public Guid NewGuidComponents() => new Guid(0x01234567, 0x8901, 0x2345, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01);
}
GuidParse:
Mean 23.4168 ns
Error 0.4823 ns
StdDev 0.4511 ns
Median 23.3622 ns
NewGuidString:
Mean 22.6531 ns
Error 0.3757 ns
StdDev 0.3514 ns
Median 22.7011 ns
NewGuidComponents:
Mean 0.0215 ns
Error 0.0170 ns
StdDev 0.0366 ns
Median 0.0000 ns
Разница огромная (×1000 в пользу числового конструктора), но в большинстве приложений создаётся мало фиксированных GUID, поэтому влияние на общую производительность минимальное. Но при старте приложения, а особенно в сценариях вроде Azure Functions или AWS Lambda, где важны cold start задержки, эта оптимизация может помочь.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔6❤5👍2🥴2
Polly остаётся основным выбором для реализации отказоустойчивости в .NET, особенно теперь, когда он быстрее в V8+, и это важно, потому что HTTP-запросы могут падать из-за сетевых или серверных проблем, создавая риск каскадных отказов, а официальная библиотека resilience в .NET всего лишь обёртка над Polly с OpenTelemetry.
Вот как сделать приложение устойчивым: читать
👉 @KodBlog
Вот как сделать приложение устойчивым: читать
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍3
Перегрузка операторов в C# 14 выходит на новый уровень и теперь вопрос только в том, стоит ли принимать такие замороченные варианты в кодовой базе, потому что подобные штуки могут взорвать мозг многим разработчикам
👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯34👍11❤7👏1
Не злоупотребляй select * в запросах к базе.
Почему это плохо:
Схема может измениться, появятся новые поля, например огромная JSONB колонка, и ты внезапно начнешь таскать лишний трафик и грузить память.
Нельзя нормально использовать covering indexes, потому что запрос требует все колонки, а не только нужные.
Запрашивай только те поля, которые реально нужны в проде.
👉 @KodBlog
Почему это плохо:
Схема может измениться, появятся новые поля, например огромная JSONB колонка, и ты внезапно начнешь таскать лишний трафик и грузить память.
Нельзя нормально использовать covering indexes, потому что запрос требует все колонки, а не только нужные.
Запрашивай только те поля, которые реально нужны в проде.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15🥴3
Разбираемся с основами работы с FakeLogger.
Мокать зависимости бывает неудобно, особенно если интерфейс не принадлежит вам, и взаимодействие с ним в коде идет через extension-методы. Так обстоит дело с ILogger из базовой инфраструктуры логирования Microsoft. Сам интерфейс ILogger довольно простой, но на практике вы почти никогда не работаете с ним напрямую. Тогда как протестировать, что вызывается правильный метод и параметры, чтобы убедиться что логирование работает корректно?
Раньше я писал о том, как замокать ILogger<> через Moq, передать его в SUT (system under test) и проверить вызовы. Это рабочий вариант, но он громоздкий, плюс код проверки приходится копировать между проектами. Не лучший вариант.
И тут появляется FakeLogger!
FakeLogger дает полный доступ ко всем данным, которые проходят через вызовы ILogger в вашем коде, и позволяет удобно писать ассерты в тестах.
Пакет немного переезжал, но сейчас он находится в Microsoft.Extensions.Diagnostics.Testing и доступен через пространство имен Microsoft.Extensions.Logging.Testing, как и ожидаешь.
После того как вы подключили NuGet-пакет в проект с тестами, можно начинать использовать его в юнит-тестах.
Простой пример
Для демонстрации возьмем небольшой SUT.
В этом сервисе нужно протестировать, что при вызове LogMe используется корректный метод логирования.
Начнем с создания экземпляра FakeLogger и SUT:
FakeLogger<> реализует нужный интерфейс, так что мы можем просто передать его в DemoService.
После выполнения Act-части теста (в AAA-подходе) можно проверять результат:
Самый простой способ — обратиться к свойству LatestRecord у fakeLogger. Оно содержит последний вызов логгера внутри SUT. На этом базовое использование заканчивается.
В этом посте мы разобрали, как использовать FakeLogger при тестировании, когда системе нужен ILogger. Инструмент позволяет проверять уровни логирования, тексты сообщений и структурированные данные, которые передаются в лог.
Вы все еще пишете свою обертку или уже переходите на FakeLogger?🐵
👉 @KodBlog
Мокать зависимости бывает неудобно, особенно если интерфейс не принадлежит вам, и взаимодействие с ним в коде идет через extension-методы. Так обстоит дело с ILogger из базовой инфраструктуры логирования Microsoft. Сам интерфейс ILogger довольно простой, но на практике вы почти никогда не работаете с ним напрямую. Тогда как протестировать, что вызывается правильный метод и параметры, чтобы убедиться что логирование работает корректно?
Раньше я писал о том, как замокать ILogger<> через Moq, передать его в SUT (system under test) и проверить вызовы. Это рабочий вариант, но он громоздкий, плюс код проверки приходится копировать между проектами. Не лучший вариант.
И тут появляется FakeLogger!
FakeLogger дает полный доступ ко всем данным, которые проходят через вызовы ILogger в вашем коде, и позволяет удобно писать ассерты в тестах.
Пакет немного переезжал, но сейчас он находится в Microsoft.Extensions.Diagnostics.Testing и доступен через пространство имен Microsoft.Extensions.Logging.Testing, как и ожидаешь.
После того как вы подключили NuGet-пакет в проект с тестами, можно начинать использовать его в юнит-тестах.
Простой пример
Для демонстрации возьмем небольшой SUT.
public class DemoService(ILogger<DemoService> logger)
{
public void LogMe()
{
logger.LogInformation("This should be logged.");
}
}
В этом сервисе нужно протестировать, что при вызове LogMe используется корректный метод логирования.
Начнем с создания экземпляра FakeLogger и SUT:
var fakeLogger = new FakeLogger<DemoService>();
var sut = new DemoService(fakeLogger);
FakeLogger<> реализует нужный интерфейс, так что мы можем просто передать его в DemoService.
После выполнения Act-части теста (в AAA-подходе) можно проверять результат:
fakeLogger.LatestRecord.Message.Should().Be("This should be logged.");Самый простой способ — обратиться к свойству LatestRecord у fakeLogger. Оно содержит последний вызов логгера внутри SUT. На этом базовое использование заканчивается.
В этом посте мы разобрали, как использовать FakeLogger при тестировании, когда системе нужен ILogger. Инструмент позволяет проверять уровни логирования, тексты сообщений и структурированные данные, которые передаются в лог.
Вы все еще пишете свою обертку или уже переходите на FakeLogger?
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15🔥3
Уже полгода в программировании, а в голове каша?
Знакомо? Дело не в вас.
Вас подвела система: куски информации без структуры и обратной связи.
Нашел годный курс Основы программирования:
✔️ Практика с первого дня, а не через месяц скучной теории
✔️ Поддержка от команды курса. Всегда отвечают на вопросы
✔️ Нет привязки ко времени. Можете учиться в своем темпе, когда угодно и сколько угодно
Прямо сейчас идет Чёрная Пятница — скидка 20%
98 630 человек уже начали учиться.
Посмотрите *1 031* реальных отзывов и начните наконец двигаться к цели.
👉 Даже можно попробовать проходить курс бесплатно:
https://stepik.org/lesson/1263653/step/1?unit=1277776
А если основы C# уже знаете, у нас есть продвинутые курсы:
https://stepik.org/org/PRO_csharp
Знакомо? Дело не в вас.
Вас подвела система: куски информации без структуры и обратной связи.
Нашел годный курс Основы программирования:
✔️ Практика с первого дня, а не через месяц скучной теории
✔️ Поддержка от команды курса. Всегда отвечают на вопросы
✔️ Нет привязки ко времени. Можете учиться в своем темпе, когда угодно и сколько угодно
Прямо сейчас идет Чёрная Пятница — скидка 20%
98 630 человек уже начали учиться.
Посмотрите *1 031* реальных отзывов и начните наконец двигаться к цели.
👉 Даже можно попробовать проходить курс бесплатно:
https://stepik.org/lesson/1263653/step/1?unit=1277776
А если основы C# уже знаете, у нас есть продвинутые курсы:
https://stepik.org/org/PRO_csharp
🔥4❤3👌3👍1
Ты можешь ловить уязвимости в коде ещё на этапе сборки.
Вот простой способ сделать это в .NET буквально за пару минут.
Представь, что можешь находить проблемы с безопасностью каждый раз, когда собираешь проект.
Статический анализ кода позволяет это сделать.
Вот пример ошибки при билде, где меня предупреждают, что мой код для хеширования пароля небезопасен.👇
Статический анализ помогает писать более безопасный и единообразный код. И его можно заставить работать на этапе сборки, включая CI пайплайны.
Если хочешь посмотреть, как настроить это с нуля, загляни в статью
Имей в виду, что такой подход не поймает абсолютно все проблемы.
Если хочешь пойти глубже, посмотри в сторону более мощных инструментов вроде SonarQube.
@KodBlog
Вот простой способ сделать это в .NET буквально за пару минут.
Представь, что можешь находить проблемы с безопасностью каждый раз, когда собираешь проект.
Статический анализ кода позволяет это сделать.
Вот пример ошибки при билде, где меня предупреждают, что мой код для хеширования пароля небезопасен.
Статический анализ помогает писать более безопасный и единообразный код. И его можно заставить работать на этапе сборки, включая CI пайплайны.
Если хочешь посмотреть, как настроить это с нуля, загляни в статью
Имей в виду, что такой подход не поймает абсолютно все проблемы.
Если хочешь пойти глубже, посмотри в сторону более мощных инструментов вроде SonarQube.
@KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3🤔1