Ef core review
Skill tunahanaliozturk/secure-dotnet-skills/skills/ef-core-review
Aegis — 12 judgment-style agent skills for secure, production-grade .NET on Azure (security, design, performance, concurrency, observability). Works with Claude Code, Codex, Cursor, Gemini.
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.
What its author says it does
Copied from the file, not written here
Use when reviewing Entity Framework Core usage — for performance (N+1, tracking, projections), correctness (transactions, concurrency), and security (raw-SQL injection, migrations).
SKILL.md
8.9 KB, 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.