EF Core — ловушки запросов, миграций, concurrency, mapping. Активируется при ef core, dbcontext, migration, N+1, AsNoTracking, Include, AsSplitQuery, IQueryable, ExecuteUpdate, cartesian explosion, GroupBy, owned types
How this skill is triggered — by the user, by Claude, or both
Slash command
/dex-skill-dotnet-ef-core:dotnet-ef-coreThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Плохо: `orders[0].Customer.Name` — каждое обращение к навигации = скрытый SQL запрос
Плохо: orders[0].Customer.Name — каждое обращение к навигации = скрытый SQL запрос
Правильно: Include(o => o.Customer) или .Select(o => new { o.Id, o.Customer.Name })
Почему: 100 заказов = 1 + 100 запросов вместо одного. Проблема тихая — нет ошибки, только медленно
Плохо: context.Products.Where(p => p.IsActive).ToListAsync() — все entities в Change Tracker
Правильно: .AsNoTracking() для данных, которые не будут изменяться
Почему: Change Tracker хранит копию каждой entity в памяти + сравнивает при DetectChanges. На 10000 записей — ощутимый overhead
Плохо: context.Orders.Include(o => o.Items).ToListAsync() — для списка нужны только Id и Total
Правильно: .Select(o => new OrderDto(o.Id, o.Total, o.Items.Count)).ToListAsync()
Почему: грузишь 20 полей × 1000 строк вместо 3 полей × 1000 строк. SQL тяжелее, трафик больше, Change Tracker раздувается
Плохо: (await repo.GetAllAsync()).Where(x => x.IsActive) — бизнес-фильтр применяется после ToList
Правильно: фильтр внутри IQueryable до материализации: repo.Query().Where(x => x.IsActive).ToListAsync()
Почему: БД тянет все строки по сети, фильтрация в памяти процесса. На больших таблицах — OOM или тайм-аут. Бизнес-условие (flag != 0, status == active) после ToList — red flag, переносить в Where
Плохо: (await repo.GetAllAsync()).GroupBy(x => x.Category).ToDictionary(...) или .Count() / .Sum() после ToList
Правильно: .GroupBy(x => x.Category).Select(g => new { g.Key, Count = g.Count() }).ToDictionaryAsync(...) — транслируется в SQL GROUP BY
Почему: EF Core транслирует большинство группировок и агрегаций в SQL. Материализация до группировки тянет все строки и ломает план запроса. Агрегации (Count, Sum, Any) должны идти SQL-запросом, не коллекцией в памяти
Плохо: Task<List<T>> FilterAsync(spec) — метод возвращает List, дальнейшая композиция невозможна
Правильно: IQueryable<T> Query(spec) для композиции на уровне сервиса / handler (или specialized read-методы типа GetByIdAsync, GetPagedAsync с проекцией внутри)
Почему: возврат List из репозитория = любой caller тянет всю сущность со всеми навигациями, теряется возможность добавить Where/Select/Take на сервере. Красивая абстракция «репозиторий скрывает EF» ценой N×объёма трафика и Change Tracker-раздувания
Общие LINQ ловушки (Count vs Any, фильтрация, коллекции) — см.
dex-skill-linq-optimization
Плохо: await context.Products.AddAsync(product) — без необходимости
Правильно: context.Products.Add(product) + await SaveChangesAsync()
Почему: AddAsync делает дополнительный запрос к БД для получения ID (HiLo sequence). Нужен ТОЛЬКО при UseHiLo(). Для Guid/client-generated id — Add() достаточно
Плохо: OwnsOne(x => x.Complexity) где Complexity — Value Object из 1 свойства; либо Owned-Type, в котором после ревью / чистки осталось одно поле (остальные удалены как избыточные)
Правильно: схлопни в плоское свойство на родителе с осмысленным именем (x.ComplexityScore). Owned-Type оправдан от 2+ полей, объединённых инвариантом, или когда планируется отдельная таблица (OwnsOne + ToTable)
Почему: Owned-Type из 1 поля = overhead конфигурации (OnModelCreating, миграция с префиксом Complexity_, OwnsOne(...).Property(...)) без выгоды. Value Object из одного значения не несёт инварианта (нечего связывать), это псевдо-абстракция. Сигнал к схлопыванию: после удаления избыточных полей в Owned-Type осталось одно — это уже не Value Object, это поле под чужим именем
Связанные ловушки: что вообще хранить в Aggregate / Owned-Type — см.
dex-skill-ddd(«Persisted-поле без потребителя»).
Плохо: soft-delete родителя (IsDeleted = true), а БД каскадно УДАЛЯЕТ дочерние записи физически
Правильно: OnDelete(DeleteBehavior.Restrict) или ClientCascade при soft-delete
Почему: EF soft-delete = update. Но FK constraint в БД настроен на CASCADE DELETE. При ручном SQL DELETE FROM parents — дочерние записи удалены навсегда
Плохо: blog.Posts.Clear(); SaveChanges() — ожидаешь что посты станут "без блога"
Правильно: понимай что required FK (non-nullable) → EF УДАЛИТ orphaned записи
Почему: PostId int (required) не может быть null. EF единственный вариант — удалить запись. Если нужно "открепить" — используй nullable FK
Плохо: два пользователя загрузили Order → оба меняют → второй тихо перезаписывает первого
Правильно: [Timestamp] public byte[] RowVersion (SQL Server) или UseXminAsConcurrencyToken() (PostgreSQL)
Почему: без concurrency token EF не проверяет что запись изменилась между read и write. Данные первого пользователя потеряны без ошибки
Плохо: SELECT ... FOR UPDATE без BeginTransactionAsync() — блокировка не работает
Правильно: await using var tx = await context.Database.BeginTransactionAsync() → FOR UPDATE → work → CommitAsync
Почему: FOR UPDATE без транзакции освобождается сразу после SELECT. Другой поток прочитает и изменит данные до вашего SaveChanges
Плохо: foreach (var p in products) { p.Price *= 1.1m; } SaveChanges() — загрузка всех entities в память
Правильно: ExecuteUpdateAsync(s => s.SetProperty(p => p.Price, p => p.Price * 1.1m)) (EF 7+)
Почему: цикл загружает 10000 entities в Change Tracker, потом генерирует 10000 UPDATE. ExecuteUpdate = один SQL запрос
Плохо: context.AuditLogs.Where(old).ExecuteDeleteAsync() — 1M строк одной транзакцией
Правильно: батчи по 1000: .Take(1000).ExecuteDeleteAsync() в цикле с Task.Delay между батчами
Почему: одна транзакция на миллион строк блокирует таблицу, раздувает WAL/transaction log, тормозит весь сервер
Плохо: Orders.Include(o => o.Items).Include(o => o.Payments).ToListAsync() — один запрос с двумя JOIN
Правильно: .AsSplitQuery() — отдельный запрос для каждого Include
Почему: два Include = cartesian product. 10 orders × 5 items × 3 payments = 150 строк вместо 10+50+30. На больших данных — из мегабайта делает гигабайт
Плохо: Orders.Where(o => o.Id == id).Include(o => o.Items).AsSplitQuery().SingleAsync()
Правильно: без AsSplitQuery — для одной entity JOIN эффективнее 2 запросов
Почему: AsSplitQuery = дополнительный roundtrip к БД. Для single entity overhead roundtrip > overhead маленького cartesian
Плохо: dotnet ef database update в CI/CD pipeline для production
Правильно: dotnet ef migrations script --idempotent → ревью SQL → применение через DBA/migration tool
Почему: EF генерирует SQL, который может содержать блокирующие ALTER TABLE, потерю данных. Без ревью — production down
Плохо: ALTER TABLE + UPDATE SET + INSERT INTO в одной миграции
Правильно: отдельная миграция для схемы, отдельная для данных
Почему: схемная миграция блокирует таблицу. Если data migration внутри неё — блокировка затягивается. При откате — неопределённое состояние
Плохо: services.AddSingleton<AppDbContext>() или DbContext в статическом поле
Правильно: AddDbContext<AppDbContext>() (Scoped по умолчанию)
Почему: Change Tracker растёт бесконечно (memory leak), stale данные, DbContext не thread-safe — concurrent access = random exceptions
Плохо: инжектить AppDbContext в BackgroundService — Scoped в Singleton
Правильно: IServiceScopeFactory → CreateScope() → resolve AppDbContext внутри scope
Почему: Scoped service captured by Singleton = один DbContext на весь lifetime приложения. Change Tracker, stale data, ObjectDisposedException
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub dex-it/claude-code-marketplace --plugin dex-skill-dotnet-ef-core