EF Core is fast when you use it deliberately
Entity Framework Core gets blamed for slow apps, but we usually find N+1 queries, unbounded includes, or accidental client-side evaluation—not broken ORM magic. EF Core 8 improved batching, JSON mapping, and raw SQL composition. This guide covers patterns we teach before anyone touches a production database at Toolliyo.
Understand change tracking
By default, EF tracks entities returned from queries so SaveChanges knows what changed. Tracking costs memory and CPU on read-heavy paths.
- Use
AsNoTracking()for read-only lists and reports. - Use
AsNoTrackingWithIdentityResolution()when you need consistent instances in one graph without updates. - Project to DTOs with
Selectso EF never materializes full entities you will not update.
var summaries = await db.Orders
.AsNoTracking()
.Where(o => o.CustomerId == customerId)
.Select(o => new OrderSummaryDto(o.Id, o.Total, o.CreatedUtc))
.ToListAsync(ct);
Only the columns you need hit the wire—less memory, less mapping, clearer intent.
N+1: the bug that keeps returning
Loading orders, then looping to load lines per order fires N+1 SQL round trips. Fix with explicit loading strategies:
Include(o => o.Lines)when you need the whole graph.- Split queries with
AsSplitQuery()on SQL Server when cartesian explosion from multiple includes hurts performance. - Projection when you only need nested fields in the UI.
Enable sensitive data logging locally and watch SQL output. One afternoon of log review beats a week of guessing.
Indexes and query filters
EF generates SQL; SQL Server needs indexes. Index foreign keys and columns in Where, OrderBy, and join keys. Use global query filters for soft delete (IsDeleted == false) but remember filtered entities disappear from normal queries—document that for the team.
Pagination done right
Never ToList() then Skip/Take in memory on large tables. Push paging to the database:
var page = await db.Products
.OrderBy(p => p.Id)
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToListAsync(ct);
For deep paging on huge tables, keyset pagination on Id or CreatedUtc beats large offsets.
Compiled queries and bulk operations
Compiled queries help hot paths executed millions of times with the same shape. EF Core 7+ bulk updates and deletes (ExecuteUpdateAsync, ExecuteDeleteAsync) avoid loading entities into memory just to change one column or remove rows.
await db.Orders
.Where(o => o.Status == OrderStatus.Expired)
.ExecuteDeleteAsync(ct);
Transactions and concurrency
Wrap multi-step writes in explicit transactions when business rules require atomicity. Use row versioning ([Timestamp] or IsRowVersion) for optimistic concurrency—catch DbUpdateConcurrencyException and merge or retry with user feedback.
When to leave EF for Dapper or raw SQL
Complex reporting, bulk ETL-shaped loads, or hand-tuned execution plans may belong in Dapper or FromSqlRaw with parameters—never string concatenation for user input. Hybrid apps use EF for OLTP and Dapper for read models; that is normal, not failure.
DbContext lifetime and pooling
Register DbContext scoped in web apps. Enable DbContext pooling for high-throughput APIs—reuse internal configuration while each request gets isolated state. Do not make DbContext singleton.
Migrations in teams
- One migration per logical change; review generated SQL before merge.
- Test migrations against a copy of production-like data volume.
- Have a rollback plan—some teams prefer idempotent SQL scripts for production.
Diagnostics toolkit
Logging with LogLevel.Information for EF SQL in development, Application Insights dependency tracking in production, MiniProfiler or tagged logging with correlation IDs. Measure p95 query duration per endpoint, not average alone.
Checklist before you ship
- Every list endpoint paginated and projected.
- No unbounded
Includechains. - Indexes verified for hot queries.
- Read paths use no-tracking or DTO projection.
- Bulk maintenance uses ExecuteUpdate/Delete where appropriate.
EF Core rewards developers who understand SQL, not those who treat the database as a black box. Master these patterns and you will ship faster queries without abandoning the productivity that brought you to EF in the first place.