Ef core review
Skill tunahanaliozturk/secure-dotnet-skills/skills/ef-core-review
Use when reviewing Entity Framework Core usage — for performance (N+1, tracking, projections), correctness (transactions, concurrency), and security (raw-SQL injection, migrations).From its SKILL.md
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill ef-core-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.9 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
EF Core Review
Directs the agent to perform a systematic review of Entity Framework Core usage across query loading strategy, write correctness, raw-SQL safety, and migration hygiene, producing a severity-rated finding with a named EF Core API fix for each issue.
When to use
- A PR introduces or modifies EF Core queries,
DbContextconfiguration, orSaveChangescall sites. - A service shows slow database response times and the cause may be N+1 loading or missing
AsNoTracking. - Raw-SQL via
FromSqlRaworExecuteSqlRawappears anywhere in the diff. - A new migration is being reviewed before it runs in staging or production.
Process
- Find the query hotspots and write paths. Locate every
DbSet<T>access, everySaveChanges/SaveChangesAsynccall, and anyFromSqlRaw/ExecuteSqlRawusage. Note which queries are inside loops. - Check the loading strategy. For each navigation property access, determine whether EF Core will lazy-load (issuing a separate query per row), eager-load via
Include, or explicitly load. Flag every place where a navigation is accessed inside a loop without a priorInclude. - Check write and transaction correctness. Confirm multi-entity writes are wrapped in a transaction and that
SaveChangesis called once per unit of work, not once per entity or per loop iteration. Verify concurrency tokens are present on entities that can be updated concurrently. - Check raw-SQL safety. For every
FromSqlRaw/ExecuteSqlRawcall, verify the SQL string is a compile-time literal or uses onlySqlParameter/DbParameterobjects — never string interpolation or concatenation of user-supplied values. PreferFromSqlInterpolatedwhen interpolation is genuinely needed; it extracts each hole as a parameterizedDbParameterautomatically. - Check migrations for data loss and idempotency. Review each
MigrationBuildermethod for destructive operations (column drops, renames, type changes) that could lose data. Confirm that migrations are idempotent when generated with--idempotentfor deployment. Check thatEnableRetryOnFailureis configured for transient-fault resilience and thatDbContextlifetime and pooling match the application host model. - Output findings with fixes. Rate each finding (Critical / High / Medium / Low), name the EF Core API that resolves it, and note whether there are sibling queries with the same defect that need the same fix.
.NET / Azure checks
- N+1 from lazy loading or missing
Include. Check whetherUseLazyLoadingProxies()is enabled and whether navigation properties are accessed inside loops. Aforeachover anOrderlist that readsorder.Customer.Namewithout.Include(o => o.Customer)issues oneSELECTper row. Fix with.Include(o => o.Customer)(eager) orentry.Reference(o => o.Customer).LoadAsync()(explicit, single call before the loop). Prefer projecting to a DTO withSelectto fetch only the columns needed. AsNoTracking()for read-only queries. AnyDbSet<T>query whose results are never passed toSaveChangesshould call.AsNoTracking()or useUseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)at the context level for read-heavy contexts. Tracked queries allocate change-tracking snapshots — on large result sets this is measurable GC pressure with no benefit. Note:AsNoTrackingdoes not change which rows are returned; it only omits the identity map and snapshot.- DTO projection instead of materializing full entities. A
.ToListAsync()that returnsList<Order>when the caller only needs order id and total unnecessarily fetches every column. Use.Select(o => new OrderSummaryDto { Id = o.Id, Total = o.Total }).ToListAsync()to push projection to the database. Returning EF entities directly from controllers also exposes unmapped columns and circular-reference serialization issues. - Raw-SQL injection via
FromSqlRaw/ExecuteSqlRaw. Any call of the formcontext.Orders.FromSqlRaw($"SELECT … WHERE Status = '{status}'")or+ userInputis SQL-injectable. RequireFromSqlInterpolated($"SELECT … WHERE Status = {status}")— EF Core extracts each{…}hole as aDbParameter, so the database always treats it as a bound value. ForExecuteSqlRaw, passSqlParameterobjects as theparams object[]argument. LINQ queries are safe because EF Core always parameterizes them. - Client-side evaluation forced by unsupported expressions. When a LINQ
Wherepredicate contains a .NET method EF Core cannot translate (e.g.,o.Description.Contains(someRegex)using a regex overload, or a custom extension method), EF Core 3+ throws at runtime rather than silently pulling all rows to the client. Run the query in development and confirm noInvalidOperationExceptionabout client-side evaluation. Rewrite using translatable members or a raw-SQL alternative. SaveChangesinside loops. Callingcontext.SaveChangesAsync()inside aforeachissues oneUPDATE/INSERTround-trip per iteration and wraps each in its own implicit transaction. Accumulate all changes and callSaveChangesAsync()once after the loop. For very large batches, considerExecuteUpdateAsync/ExecuteDeleteAsync(EF Core 7+) which translate to set-based SQL without loading entities.- Concurrency tokens and transactions for multi-entity writes. Entities that can be updated by concurrent requests need a concurrency token: either a
[Timestamp]/byte[]property mapped with.IsRowVersion()(SQL Serverrowversion) or a[ConcurrencyCheck]scalar property. Without a token, the last writer silently wins. Multi-entity write operations that must be atomic must use an explicitIDbContextTransactionviacontext.Database.BeginTransactionAsync()and commit or roll back as a unit. - Migrations: destructive operations, idempotency, and resilience. Review
MigrationBuilder.DropColumn,RenameColumn, and column-type changes for data loss. A column drop with no preceding data-migration step loses data permanently. Confirmcontext.Database.MigrateAsync()is not called on startup in a multi-instance deployment (use a one-shot migration job instead). ConfirmEnableRetryOnFailure(maxRetryCount: 5)is set inUseSqlServer/UseNpgsqloptions for transient Azure SQL / Postgres errors. ConfirmDbContextis registered withAddDbContext<T>(scoped lifetime) orAddDbContextPool<T>(pooled, scoped, all state reset between requests) — never as a singleton, which causes cross-request state pollution.
Red flags
| Signal | Why it matters |
|---|---|
context.Orders.FromSqlRaw($"… WHERE Status = '{status}'") | String-interpolated raw SQL passes user input directly into the query; the interpolated hole is not parameterized by FromSqlRaw, making it trivially injectable. Use FromSqlInterpolated. |
Navigation property accessed inside foreach with no prior Include | Issues one SELECT per loop iteration (N+1). On a list of 500 rows this is 501 round-trips; on a large dataset it is a liveness risk. |
.ToList() followed by .Where(…) in memory | EF Core fetches every row from the database and then filters in the .NET process. Use .Where(…).ToListAsync() to push the predicate to SQL. |
await context.SaveChangesAsync() inside a loop body | Each call opens and closes an implicit transaction. Accumulate changes first; call SaveChangesAsync once outside the loop. |
Controller action returns IEnumerable<Order> (EF entity) directly | Exposes every column including internal fields, risks serialization cycles on navigation properties, and leaks the data model to the API contract. Project to a DTO. |
Migration with DropColumn and no prior data-migration step | Drops data permanently on the next deploy. Add a data-migration migration before the destructive one, or move the data in the same migration using migrationBuilder.Sql. |
No .AsNoTracking() on read-only queries | Every tracked entity allocates a change-tracking snapshot. On a query returning thousands of rows this wastes memory and GC time with no benefit when results are never saved. |
DbContext registered as AddSingleton<AppDbContext> | A singleton DbContext is shared across all requests and across Task.WhenAll parallel paths. DbContext is not thread-safe; concurrent access corrupts its internal state map. |
Example
Related skills
- dotnet-performance-review — use for broad .NET performance review beyond EF Core queries (allocations, async, caching).
- dotnet-security-review — use to catch raw-SQL injection and other security issues across the full service.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.