ORM & Data Access · .NET

Entity Framework Core

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.

C# / .NETLINQCode-FirstChange Tracking
← Back to Database Side
Quick Facts

At a Glance

Core Ideas

  • DbContext: the unit-of-work and connection holder — typically scoped per HTTP request.
  • DbSet<T>: the queryable collection for an entity type. context.Orders.Where(...).
  • LINQ-to-Entities: queries are C# expressions translated to SQL on the fly.
  • Change Tracker: snapshots loaded entities; SaveChanges() writes only what differs.
  • Migrations: code-first by default — model changes generate timestamped migration classes.
The Pieces

What You'll Touch

EF Core (current)

The cross-platform rewrite. Versions track .NET releases — EF 8/9 ship alongside .NET 8/9. The one to use for new work.

EF6 (legacy)

The .NET Framework era. Still alive in long-lived enterprise apps; not where new development goes.

Providers

SQL Server, PostgreSQL (Npgsql), MySQL, SQLite, Cosmos DB, Oracle. Same LINQ; different SQL.

Dapper

The micro-ORM many teams use alongside EF — raw SQL, fast object mapping, no tracking. Reach for it on hot paths.

EF Power Tools

Visual Studio extension for reverse-engineering existing schemas (database-first scaffolding).

LINQPad

The scratchpad — write LINQ against your DbContext, see SQL and results instantly.

Why It Wins

What EF Core Buys You

LINQ Is the Killer Feature

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.

Code-First Migrations

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.

Async End-to-End

ToListAsync, FirstOrDefaultAsync, SaveChangesAsync — async is native, not bolted on. Important for any web tier that has to scale request concurrency.

Sane Defaults with ASP.NET Core

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.

The Sharp Edges

Where People Get Cut

Client-Side Evaluation Cliff

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.

Tracking vs No-Tracking

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.

The N+1 Problem (Same as Everywhere)

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.

SaveChanges Is Not a Bulk Insert

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.

DbContext Is Not Thread-Safe

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.

Patterns

How Teams Actually Use It

  • Repository wrappers are usually unnecessary. 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.
  • Use projection (Select) for read endpoints. Materialize DTOs directly instead of entities — smaller queries, no tracking overhead.
  • Compiled queries for hot paths — EF.CompileAsyncQuery caches the LINQ-to-SQL translation.
  • Drop to Dapper for reporting queries and CQRS read sides — keeps EF for the write model where tracking earns its cost.
  • Always check the SQL. ToQueryString() in EF 5+, or log to console in dev. The translator is good but not magic.
Continue

Related