Entity Framework Core

Entity Framework Core (EF Core) is the object-relational mapper for .NET. You describe your data as C# classes and a DbContext, write queries in LINQ, and EF Core translates them into SQL for SQL Server, PostgreSQL, SQLite, MySQL, and other databases. It tracks changes to loaded entities and saves them in a single unit of work, and migrations evolve the database schema alongside the code.

EF Core removes a great deal of data-access boilerplate, but it is not a way to avoid understanding SQL. Most production problems with EF Core — slow pages, timeouts, unexpected memory use — come from queries that translate into something other than what the developer imagined. Knowing how translation, tracking, and loading work keeps it fast.

TL;DR

Quick Example

A context, an entity, and efficient queries:

Registration in ASP.NET Core, with context pooling:

Core Concepts

Modeling

EF Core builds a model from conventions (an Id property is the key; a navigation plus foreign key forms a relationship), data annotations, and the fluent API in OnModelCreating. The fluent API is the most expressive: keys, indexes, precision, relationships and delete behavior, table splitting, owned and complex types for value objects, value converters for enums or strongly typed IDs, and JSON columns. Global query filters apply conditions automatically — useful for soft deletes and multi-tenancy, as long as everyone knows they exist.

Query Translation

A LINQ query is an expression tree until it is enumerated. EF Core translates it into SQL; methods it cannot translate cause an exception rather than silent client-side evaluation (except in the final projection). Inspect the SQL with ToQueryString(), logging (LogTo), or a profiler. FromSql and SqlQuery run raw SQL when LINQ is not the right tool — with interpolated values automatically turned into parameters.

Change Tracking

Entities loaded by a tracking query are snapshotted. When SaveChangesAsync runs, EF Core detects changes and issues inserts, updates, and deletes in a transaction, in dependency order. Tracking costs memory and CPU, so read-only paths should use AsNoTracking() or projections. A DbContext is a short-lived unit of work: one per request, never shared across threads.

Loading Related Data

Concurrency

Configure a concurrency token — rowversion on SQL Server, xmin on PostgreSQL, or a version column. Updates include the original value in their WHERE clause; if another writer changed the row, EF Core throws DbUpdateConcurrencyException, and the application decides whether to retry, merge, or report a conflict.

Migrations

Review generated migrations like any code: they can drop columns or rebuild tables. In production, apply migrations as an explicit deployment step — an idempotent SQL script or a migration bundle — rather than calling Database.Migrate() from every application instance at startup. Use expand-and-contract changes so old and new application versions work during rollout. See Database Migrations.

Providers

Providers differ in translation support and types; test with the provider you deploy on.

Performance Habits

Best Practices

Keep the DbContext Short-Lived

Create one per request or unit of work through dependency injection. Long-lived contexts accumulate tracked entities and stale data.

Read the SQL

Log generated SQL in development and review it for hot queries. If the SQL surprises you, rewrite the LINQ or use raw SQL.

Test Against a Real Database

The in-memory provider does not behave like a relational database (no transactions, constraints, or SQL translation). Use SQLite carefully, or better, the real engine in containers. See Object-Relational Mapping.

Make Migrations a Reviewed Deployment Step

Generate scripts in CI, review them, and apply them once per deployment with appropriate permissions.

Common Mistakes

N+1 Queries in Loops

Calling ToList Too Early

Migrating at Startup From Every Instance

FAQ

What is Entity Framework Core?

The object-relational mapper for .NET. It maps C# classes to database tables, translates LINQ queries to SQL, tracks changes to loaded objects, and manages schema changes with migrations.

Is EF Core slow?

Not inherently. Most performance problems come from N+1 queries, loading too much data, unnecessary tracking, or untranslatable queries. With projections, no-tracking reads, proper indexes, and bulk operations, EF Core performs well for most workloads.

Should I use EF Core or Dapper?

EF Core offers change tracking, migrations, and LINQ; Dapper is a thin, very fast mapper over hand-written SQL. Many teams use EF Core for most work and Dapper or raw SQL for specific performance-critical queries.

How do I handle concurrency conflicts?

Configure a concurrency token such as a row version. EF Core then throws DbUpdateConcurrencyException when a row changed since it was read, and your code decides whether to retry, merge, or report the conflict.

Should I use the in-memory provider for tests?

Generally not for data-access tests, because it does not behave like a relational database. Use the real database engine in a container, or SQLite with awareness of its differences.

Related Topics

References