AI в dotnet — RAG-система на Postgres и Ollama
Для начала, что такое RAG?
RAG это архитектура, которая усиливает генеративные LLM, добавляя к ним классические механики поиска ( движки, базы данных и так далее. )
За счет этого модель может выдавать ответы точнее, актуальнее и привязанные к контексту, опираясь и на ваши данные, и на внешние знания.
Пример:
Допустим, компании нужен бот, который мгновенно выдает точные ответы на вопросы о продуктах и сервисах. Вместо того чтобы полагаться только на предобученную LLM, у которой может не быть свежей инфы по компании, бот строится как RAG-система.
Как это работает:
Запрос пользователя/ клиент спрашивает, например, какая у него политика возврата по последнему заказу.
Этап извлечения/ система сначала ищет данные во внутренней документации, базе знаний или БД (FAQ, регламенты, история заказов).
Генерация с контекстом/ найденные данные передаются в LLM, и та формирует структурированный, корректный ответ.
Ответ/ бот говорит что-то вроде:
Вот реализация RAG на .NET.
Для модели использовалась embedding-модель Mistral в Ollama. Установка занимает минут десять.
В качестве векторной базы PostgreSQL.
Гайд по внедрению здесь
👉 @KodBlog
Для начала, что такое RAG?
RAG это архитектура, которая усиливает генеративные LLM, добавляя к ним классические механики поиска ( движки, базы данных и так далее. )
За счет этого модель может выдавать ответы точнее, актуальнее и привязанные к контексту, опираясь и на ваши данные, и на внешние знания.
Пример:
Допустим, компании нужен бот, который мгновенно выдает точные ответы на вопросы о продуктах и сервисах. Вместо того чтобы полагаться только на предобученную LLM, у которой может не быть свежей инфы по компании, бот строится как RAG-система.
Как это работает:
Запрос пользователя/ клиент спрашивает, например, какая у него политика возврата по последнему заказу.
Этап извлечения/ система сначала ищет данные во внутренней документации, базе знаний или БД (FAQ, регламенты, история заказов).
Генерация с контекстом/ найденные данные передаются в LLM, и та формирует структурированный, корректный ответ.
Ответ/ бот говорит что-то вроде:
политика возврата — 30 дней; твой заказ сделан 15 дней назад, значит возврат доступен; нужно запустить процесс?
Вот реализация RAG на .NET.
Для модели использовалась embedding-модель Mistral в Ollama. Установка занимает минут десять.
В качестве векторной базы PostgreSQL.
Гайд по внедрению здесь
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤3
Нашли полезный GitHub-репозиторий с личным набором рабочих скриптов для LINQPad
Внутри .linq-скрипты, заточенные под .NET-проекты: быстрые запросы к базе через LINQPad вместо поднятия отдельных утилит, мелкие проверки, конвертеры данных и прочие «одноразовые» штуки, которые на практике всплывают постоянно.🍿
👉 @KodBlog
Внутри .linq-скрипты, заточенные под .NET-проекты: быстрые запросы к базе через LINQPad вместо поднятия отдельных утилит, мелкие проверки, конвертеры данных и прочие «одноразовые» штуки, которые на практике всплывают постоянно.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8❤7🥰3🌚1
Использование сортируемых UUID/GUID в Entity Framework
В .NET 9 появились методы Guid.CreateVersion7() и Guid.CreateVersion7(DateTimeOffset), которые генерируют UUID/GUID, упорядочиваемые по времени создания. Это особенно удобно, когда база должна хранить записи в хронологическом порядке, да и в плане производительности есть свои плюсы. Сейчас в Entity Framework нет встроенного механизма, позволяющего подменить генерацию новых GUID на эти методы. Но это легко сделать вручную.
Сначала нужно написать свой ValueGenerator, который будет выдавать GUID через Guid.CreateVersion7():
Подключение:
Готово. Теперь Id у MyEntity будет генерироваться как UUID версии 7. Если хотите, чтобы в EF появился более удобный встроенный способ для таких ключей, можно проголосовать за соответствующий тикет: https://github.com/dotnet/efcore/issues/34158
👉 @KodBlog
В .NET 9 появились методы Guid.CreateVersion7() и Guid.CreateVersion7(DateTimeOffset), которые генерируют UUID/GUID, упорядочиваемые по времени создания. Это особенно удобно, когда база должна хранить записи в хронологическом порядке, да и в плане производительности есть свои плюсы. Сейчас в Entity Framework нет встроенного механизма, позволяющего подменить генерацию новых GUID на эти методы. Но это легко сделать вручную.
Сначала нужно написать свой ValueGenerator, который будет выдавать GUID через Guid.CreateVersion7():
public class UUIDv7Generator : ValueGenerator<Guid>
{
public override bool GeneratesTemporaryValues => false;
public override Guid Next(EntityEntry entry)
=> Guid.CreateVersion7();
}
Подключение:
public class MyDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<MyEntity>()
.Property(e => e.Id)
.HasValueGenerator<UUIDv7Generator>()
.ValueGeneratedOnAdd();
}
}
Готово. Теперь Id у MyEntity будет генерироваться как UUID версии 7. Если хотите, чтобы в EF появился более удобный встроенный способ для таких ключей, можно проголосовать за соответствующий тикет: https://github.com/dotnet/efcore/issues/34158
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤5
Кто ещё любит точечно использовать именованные аргументы, чтобы вызовы методов в C# читались понятнее?
👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
❤32💯15👍9🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
Бывало, что хочется принять автодополнение не целиком, а только кусок и сразу одним кликом? Теперь можно кликнуть прямо по подсказке и принять её только до позиции курсора.
👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21👍5
Разбираем курсорную пагинацию.
Пагинация (разбиение на страницы) критична, когда нужно эффективно отдавать большие наборы данных. Хотя офсетная пагинация (offset pagination) используется повсеместно, курсорная пагинация (cursor pagination) в некоторых сценариях даёт интересные преимущества. Она особенно полезна для real-time лент, интерфейсов с бесконечной прокруткой и API, где важна производительность на масштабе, например для журналов активности или потоков событий, где пользователи часто пролистывают большие объёмы данных. Разберём детали реализации и обсудим, где каждый подход уместнее.
Сделаем простое хранилище заметок пользователя на базе такого объекта:
Традиционный подход - офсетная пагинация
Офсетная пагинация использует Skip и Take: мы пропускаем заданное число строк и берём фиксированное количество. Обычно это транслируется в OFFSET и LIMIT в SQL:
Обратите внимание, мы сортируем результаты по Date и Id по убыванию. Это нужно, чтобы разбиение на страницы было стабильным. Вот сгенерированный SQL (в PostgreSql) для офсетной пагинации:
Ограничения офсетной пагинации:
Производительность падает по мере роста смещения, потому что базе приходится сканировать и отбрасывать все строки до OFFSET;
Есть риск потерять элементы или получить дубликаты, если данные меняются между запросами страниц;
Результаты могут быть несогласованными при параллельных обновлениях.
👉 @KodBlog
Пагинация (разбиение на страницы) критична, когда нужно эффективно отдавать большие наборы данных. Хотя офсетная пагинация (offset pagination) используется повсеместно, курсорная пагинация (cursor pagination) в некоторых сценариях даёт интересные преимущества. Она особенно полезна для real-time лент, интерфейсов с бесконечной прокруткой и API, где важна производительность на масштабе, например для журналов активности или потоков событий, где пользователи часто пролистывают большие объёмы данных. Разберём детали реализации и обсудим, где каждый подход уместнее.
Сделаем простое хранилище заметок пользователя на базе такого объекта:
public record UserNote (
Guid Id,
Guid UserId,
string? Note,
DateOnly Date
);
Традиционный подход - офсетная пагинация
Офсетная пагинация использует Skip и Take: мы пропускаем заданное число строк и берём фиксированное количество. Обычно это транслируется в OFFSET и LIMIT в SQL:
var query = dbContext.UserNotes
.OrderByDescending(x => x.Date)
.ThenByDescending(x => x.Id);
// При офсетной пагинации мы обычно сначала считаем общее количество элементов
var total = await query
.CountAsync(cancellationToken);
var pages = (int)Math.Ceiling(total / (double)pageSize);
var query = dbContext.UserNotes
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
Обратите внимание, мы сортируем результаты по Date и Id по убыванию. Это нужно, чтобы разбиение на страницы было стабильным. Вот сгенерированный SQL (в PostgreSql) для офсетной пагинации:
-- запрос общего количества
SELECT count(*)::int FROM user_notes AS u;
-- запрос данных
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
ORDER BY u.date DESC, u.id DESC
LIMIT @pageSize OFFSET @offset;
Ограничения офсетной пагинации:
Производительность падает по мере роста смещения, потому что базе приходится сканировать и отбрасывать все строки до OFFSET;
Есть риск потерять элементы или получить дубликаты, если данные меняются между запросами страниц;
Результаты могут быть несогласованными при параллельных обновлениях.
Please open Telegram to view this post
VIEW IN TELEGRAM
👎9🔥6❤3
Это огромная база бесплатных курсов по IT/CS/дизайну/бизнесу, где после прохождения можно получить сертификат или цифровой бейдж.
Для бэкендеров тут есть подборка бесплатных курсов с сертификатами по API и REST (FreeCodeCamp, HackerRank), Postman, базам данных (Saylor) и MongoDB (MongoDB University), плюс SQL-база от Kaggle
👉 @KodBlog
Для бэкендеров тут есть подборка бесплатных курсов с сертификатами по API и REST (FreeCodeCamp, HackerRank), Postman, базам данных (Saylor) и MongoDB (MongoDB University), плюс SQL-база от Kaggle
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3
Ты знаешь, что ArgumentNullException.ThrowIfNull можно использовать даже при таргете на .NET Framework, если задействовать extension members и C# 14?
По сути, ты можешь собрать свою кастомную библиотеку-полифилл, которая будет прятать различия при таргетинге на разные target frameworks.
👉 @KodBlog
По сути, ты можешь собрать свою кастомную библиотеку-полифилл, которая будет прятать различия при таргетинге на разные target frameworks.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍3
C# Portal | Программирование
Разбираем курсорную пагинацию. Пагинация (разбиение на страницы) критична, когда нужно эффективно отдавать большие наборы данных. Хотя офсетная пагинация (offset pagination) используется повсеместно, курсорная пагинация (cursor pagination) в некоторых сценариях…
Курсорная пагинация использует контрольную точку (курсор), чтобы получить следующий набор результатов. Эта контрольная точка обычно представляет собой уникальный идентификатор или комбинацию полей, задающих порядок сортировки.
Используем поля Date и Id, чтобы построить курсор для нашей таблицы UserNotes. Курсор это композиция этих двух полей, и она позволяет эффективно делать пагинацию:
Порядок сортировки тот же, что и в примере с офсетной пагинацией
Но при курсорной пагинации сортировка критична для консистентных результатов. Так как Date в нашей таблице не уникален, мы добавляем Id для корректной обработки границ страниц. Это гарантирует, что при пагинации мы не пропустим и не задублируем элементы.
Вот сгенерированный SQL (PostgreSQL) для курсорной пагинации:
Обрати внимание: в запросе нет OFFSET. Производительность курсорной пагинации остаётся постоянной независимо от номера страницы, потому что мы напрямую ищем строки по значениям курсора. Это заметно эффективнее, чем OFFSET. Это огромное преимущество по сравнению с офсетной пагинацией, особенно на больших наборах данных.
COUNT при курсорной пагинации тоже не нужен, потому что мы не считаем общее число элементов. Хотя он может понадобиться, если тебе нужно заранее показать общее количество страниц.
Ограничения курсорной пагинации:
- Если пользователям нужно динамически менять поля сортировки, курсорная пагинация становится сильно сложнее, потому что курсор должен включать все условия сортировки.
- Пользователи не могут перейти к конкретному номеру страницы, только последовательно листать страницы.
- Реализовать корректно сложнее, чем офсетную пагинацию, особенно на границах страниц и при обеспечении корректной сортировки.
👉 @KodBlog
Используем поля Date и Id, чтобы построить курсор для нашей таблицы UserNotes. Курсор это композиция этих двух полей, и она позволяет эффективно делать пагинацию:
var query = dbContext.UserNotes.AsQueryable();
if (date != null && lastId != null)
{
// Используем курсор для выборки следующего чанка результатов
// При прямой сортировке нужно заменить > на <
query = query
.Where(x => x.Date < date ||
(x.Date == date && x.Id <= lastId));
}
// Забираем на 1 элемент больше
var items = await query
.OrderByDescending(x => x.Date)
.ThenByDescending(x => x.Id)
.Take(limit + 1)
.ToListAsync(cancellationToken);
// Определяем параметры для следующей страницы
var hasMore = items.Count > limit;
DateOnly? nextDate =
hasMore ? items[^1].Date : null;
Guid? nextId =
hasMore ? items[^1].Id : null;
// Удаляем "лишний" результат, если он есть
if (hasMore)
items.RemoveAt(items.Count - 1);
Порядок сортировки тот же, что и в примере с офсетной пагинацией
Но при курсорной пагинации сортировка критична для консистентных результатов. Так как Date в нашей таблице не уникален, мы добавляем Id для корректной обработки границ страниц. Это гарантирует, что при пагинации мы не пропустим и не задублируем элементы.
Вот сгенерированный SQL (PostgreSQL) для курсорной пагинации:
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 @limit;
Обрати внимание: в запросе нет OFFSET. Производительность курсорной пагинации остаётся постоянной независимо от номера страницы, потому что мы напрямую ищем строки по значениям курсора. Это заметно эффективнее, чем OFFSET. Это огромное преимущество по сравнению с офсетной пагинацией, особенно на больших наборах данных.
COUNT при курсорной пагинации тоже не нужен, потому что мы не считаем общее число элементов. Хотя он может понадобиться, если тебе нужно заранее показать общее количество страниц.
Ограничения курсорной пагинации:
- Если пользователям нужно динамически менять поля сортировки, курсорная пагинация становится сильно сложнее, потому что курсор должен включать все условия сортировки.
- Пользователи не могут перейти к конкретному номеру страницы, только последовательно листать страницы.
- Реализовать корректно сложнее, чем офсетную пагинацию, особенно на границах страниц и при обеспечении корректной сортировки.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4❤3👍2
C#
🔹 Модификатор in позволяет компилятору создать временную переменную для аргумента и передать только чтение по ссылке.
🔹 Методы с in параметрами потенциально получают оптимизацию производительности за счёт избежания копирования больших структур.
Подробнее у Microsoft: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters#in-parameter-modifier
Пример в репозитории: https://github.com/karenpayneoregon/learning-topics/tree/master/InParameterSampleApp
👉 @KodBlog
in параметрПодробнее у Microsoft: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters#in-parameter-modifier
Пример в репозитории: https://github.com/karenpayneoregon/learning-topics/tree/master/InParameterSampleApp
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15🤯2😁1
Хочешь сделать встроенные покупки в приложении на .NET MAUI? Тогда эта статья для тебя.
Тут собрали пример, который показывает, как работать со встроенными покупками в MAUI для iOS, Android и Windows.
А если нужна более сложная обработка платежей, включая серверную часть, советую библиотеку RevenueCat. Вот для неё обёртка под MAUI.
👉 @KodBlog
Тут собрали пример, который показывает, как работать со встроенными покупками в MAUI для iOS, Android и Windows.
А если нужна более сложная обработка платежей, включая серверную часть, советую библиотеку RevenueCat. Вот для неё обёртка под MAUI.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍4🔥3
Массовая выборка данных в EF Core
5 методов, которые стоит знать
При работе с Entity Framework Core часто нужно вытащить из базы несколько сущностей по списку ID или значений.
Обычно делают через Contains:
Для небольших списков это нормально, но на больших объёмах начинаются проблемы:
→ Просадка по скорости. Даже с индексами SQL-условие WHERE IN тормозит всё сильнее по мере роста количества параметров.
→ Упираемся в лимит параметров. В SQL Server жёсткий предел: 2 100 параметров на запрос. Можно дробить на батчи на уровне SQL, но в EF Core это не всегда удобно, особенно когда есть доп. фильтры и join’ы.
→ Проблемы с памятью и соединениями. Много походов в базу ест память и дольше держит соединения открытыми, из-за чего можно легко упереться в исчерпание пула соединений.
Это реально всплывает в задачах вроде синхронизации каталога, обработки массовых заказов или обновления остатков по тысячам позиций.
В чистом SQL обычно обходят лимит через временную таблицу или параметр-таблицу.
Но такие варианты требуют писать сырой SQL и теряются плюсы EF Core -» строгая типизация запросов, отслеживание изменений и поддержка навигационных свойств.
Есть более удобное решение --» библиотека Entity Framework Extensions даёт специальные методы для массовой выборки, которые закрывают эти проблемы.
Внутри она использует временные таблицы, чтобы обойти лимит параметров и ускорить запросы. Вместо передачи тысяч значений в WHERE IN она делает так:
- создаёт временную таблицу в базе
- вставляет туда значения фильтра
- джойнится таблицей сущностей к временной таблице
- возвращает отфильтрованный результат
- автоматически удаляет временную таблицу
На больших наборах данных это заметно ускоряет выборку.
В Entity Framework Extensions есть пять основных методов для массовой выборки:
→ WhereBulkContains
→ WhereBulkNotContains
→ BulkRead
→ WhereBulkContainsFilterList
→ WhereBulkNotContainsFilterList
👉 @KodBlog
5 методов, которые стоит знать
При работе с Entity Framework Core часто нужно вытащить из базы несколько сущностей по списку ID или значений.
Обычно делают через Contains:
var products = await dbContext.Products
.Where(p => productIds.Contains(p.Id))
.ToListAsync();
Для небольших списков это нормально, но на больших объёмах начинаются проблемы:
→ Просадка по скорости. Даже с индексами SQL-условие WHERE IN тормозит всё сильнее по мере роста количества параметров.
→ Упираемся в лимит параметров. В SQL Server жёсткий предел: 2 100 параметров на запрос. Можно дробить на батчи на уровне SQL, но в EF Core это не всегда удобно, особенно когда есть доп. фильтры и join’ы.
→ Проблемы с памятью и соединениями. Много походов в базу ест память и дольше держит соединения открытыми, из-за чего можно легко упереться в исчерпание пула соединений.
Это реально всплывает в задачах вроде синхронизации каталога, обработки массовых заказов или обновления остатков по тысячам позиций.
В чистом SQL обычно обходят лимит через временную таблицу или параметр-таблицу.
Но такие варианты требуют писать сырой SQL и теряются плюсы EF Core -» строгая типизация запросов, отслеживание изменений и поддержка навигационных свойств.
Есть более удобное решение --» библиотека Entity Framework Extensions даёт специальные методы для массовой выборки, которые закрывают эти проблемы.
Внутри она использует временные таблицы, чтобы обойти лимит параметров и ускорить запросы. Вместо передачи тысяч значений в WHERE IN она делает так:
- создаёт временную таблицу в базе
- вставляет туда значения фильтра
- джойнится таблицей сущностей к временной таблице
- возвращает отфильтрованный результат
- автоматически удаляет временную таблицу
На больших наборах данных это заметно ускоряет выборку.
В Entity Framework Extensions есть пять основных методов для массовой выборки:
→ WhereBulkContains
→ WhereBulkNotContains
→ BulkRead
→ WhereBulkContainsFilterList
→ WhereBulkNotContainsFilterList
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11
Media is too big
VIEW IN TELEGRAM
Запускайте GitHub Actions локально в VS Code!
Расширение GitHub Local Actions позволяет запускать рабочий процесс и отдельные job’ы без коммитов и пушей, используя CLI-инструмент nektos/act. Интерфейс сделан максимально похоже на официальный GitHub Actions, так что вливаться легко.
Можно триггерить стандартные GitHub-события, смотреть историю запусков и логи, а также настраивать secrets, переменные, inputs, runners и payload’ы для выполнения. Отличный способ ускорить разработку и отладку CI🧃
👉 @KodBlog
Расширение GitHub Local Actions позволяет запускать рабочий процесс и отдельные job’ы без коммитов и пушей, используя CLI-инструмент nektos/act. Интерфейс сделан максимально похоже на официальный GitHub Actions, так что вливаться легко.
Можно триггерить стандартные GitHub-события, смотреть историю запусков и логи, а также настраивать secrets, переменные, inputs, runners и payload’ы для выполнения. Отличный способ ускорить разработку и отладку CI
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Хорошие новости для пользователей 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