EF Core is the canonical .NET ORM. You write LINQ against DbSet<T>, EF translates to SQL, materializes into objects, and tracks the changes you make. It's the default in nearly every ASP.NET Core project that talks to a relational database.
context.Orders.Where(...).SaveChanges() writes only what differs.The cross-platform rewrite. Versions track .NET releases — EF 8/9 ship alongside .NET 8/9. The one to use for new work.
The .NET Framework era. Still alive in long-lived enterprise apps; not where new development goes.
SQL Server, PostgreSQL (Npgsql), MySQL, SQLite, Cosmos DB, Oracle. Same LINQ; different SQL.
The micro-ORM many teams use alongside EF — raw SQL, fast object mapping, no tracking. Reach for it on hot paths.
Visual Studio extension for reverse-engineering existing schemas (database-first scaffolding).
The scratchpad — write LINQ against your DbContext, see SQL and results instantly.
Composable, type-checked queries that refactor with the model. Rename a property and every query updates. Add a .Where() conditionally without string-concatenating SQL. The compiler catches what runtime errors would catch in dynamically-typed ORMs.
dotnet ef migrations add AddOrderStatus diffs the model against the last snapshot and emits an Up/Down pair. Reviewable in PRs, deterministic, runs in CI. The default workflow most teams settle on.
ToListAsync, FirstOrDefaultAsync, SaveChangesAsync — async is native, not bolted on. Important for any web tier that has to scale request concurrency.
builder.Services.AddDbContext<AppDbContext>() wires lifetime, connection string, logging, and pooling in one line. The DI container hands a fresh DbContext to each request; you almost never write the plumbing.
Older EF Core silently ran un-translatable LINQ in memory after pulling the whole table. EF 3+ throws instead — louder, but you'll meet it. The fix is restating the query so the provider can translate it, or projecting earlier.
Read-only queries should use .AsNoTracking(). Without it, every entity is snapshotted into the change tracker — slower, and a memory leak waiting to happen on long-lived contexts. Many teams set QueryTrackingBehavior.NoTracking as the default and opt in.
Forget .Include() and lazy loading silently fires a query per accessed navigation. Lazy loading is off by default in EF Core for exactly this reason — leave it off and use Include/ThenInclude or projection explicitly.
Adding 100k entities and calling SaveChanges sends 100k INSERTs in batches. Fine for hundreds, terrible for millions. Reach for SqlBulkCopy, EFCore.BulkExtensions, or the COPY protocol on Postgres.
One context per scope. Sharing across Task.WhenAll calls produces undefined behavior. The DI scope handles this for HTTP — for background jobs and parallel work, create a context per unit.
DbContext + DbSet already implement the pattern. Adding an extra interface buys testability that an in-memory provider or a real test database gives you anyway.Select) for read endpoints. Materialize DTOs directly instead of entities — smaller queries, no tracking overhead.EF.CompileAsyncQuery caches the LINQ-to-SQL translation.ToQueryString() in EF 5+, or log to console in dev. The translator is good but not magic.