AlgoMaster Logo

Entity Framework Core Basics

Last Updated: May 17, 2026

13 min read

Most C# applications eventually need to store data somewhere, and most of that data lives in a relational database: products and prices, customers and orders, carts and reviews. Writing the SQL by hand and gluing the results back onto C# objects is tedious and error-prone, which is why almost every modern .NET app reaches for an ORM. This lesson covers what an ORM is, what Entity Framework Core gives you, which databases it supports, the NuGet packages you need to install, and the smallest possible end-to-end example you can run on your machine without setting up a database server.

What an ORM Is and Why It Exists

An ORM, short for Object-Relational Mapper, is a library that bridges two worlds: the relational world of tables, rows, and SQL, and the object world of classes, properties, and method calls. You define your data as plain C# classes (Product, Order, Customer), the ORM figures out how those classes map to tables and columns, and from then on you read and write data by working with objects instead of writing SQL by hand.

To see why that matters, look at the alternative. Without an ORM, fetching a single product from a SQL Server database in C# looks like this:

That's a lot of plumbing for one product. You're writing SQL strings, dealing with parameters by index, opening connections, walking a reader, and converting columns to typed values one at a time. Multiply that by every query in a real app and you've written more boilerplate than business logic. You also have to remember to dispose readers and connections in the right order, and a mistake there leaks resources.

With an ORM, the same fetch becomes a one-liner:

That's the whole point. The ORM handles connection management, parameter binding, type conversion, and result mapping. You spend your time on what your app does, not on the wiring underneath.

ORMs do a few other things worth naming. They track which objects you've changed in memory so they can generate the right UPDATE statements when you call SaveChanges. They translate LINQ queries into SQL, so you can write db.Products.Where(p => p.Price < 50) and get the equivalent SELECT ... WHERE Price < 50 on the server. They manage schema for you, so you can describe your model in C# and let the tool generate matching tables.

The trade-off is that ORMs add a layer of abstraction, and abstractions sometimes leak. You can write a LINQ query that compiles fine but generates terrible SQL. You can load more data than you meant to. You can run into the famous "N+1 query" problem if you're not careful. For now, the picture you want is "ORM removes 90% of the boilerplate; you learn the 10% of pitfalls it introduces."

What EF Core Is

Entity Framework Core (EF Core) is Microsoft's modern ORM for .NET. It's open source, cross-platform, and ships independently of the .NET runtime as a NuGet package. The current major version is EF Core 8, which targets .NET 8 and is the recommended choice for new applications as of late 2024.

There's some history worth knowing. The original Entity Framework (often called "EF6" for its last major version) shipped in 2008 and was tightly coupled to Windows and the full .NET Framework. It worked, but it was heavy, hard to extend, and difficult to move forward. When Microsoft rewrote .NET to be cross-platform (.NET Core, now just .NET), they also rewrote the ORM. The result, Entity Framework Core, started fresh in 2016 with a leaner architecture, a wider set of supported databases, and async-first APIs. Today, EF Core is the only Entity Framework version under active development; EF6 still receives bug fixes but no new features.

EF Core does four main things:

  • Models your data. You define plain C# classes (called entities), and EF Core figures out how each class maps to a database table.
  • Tracks changes. When you load an entity, modify a property, and call SaveChanges, EF Core generates the right UPDATE to persist the change.
  • Translates LINQ to SQL. You write LINQ queries against your entities, and EF Core translates them into provider-specific SQL.
  • Manages schema. You describe your model in C# code, and EF Core can generate matching database tables and keep them in sync as the model evolves through migrations.

The piece that holds all of this together is the DbContext, a class you derive from Microsoft.EntityFrameworkCore.DbContext to represent a session with the database. Inside the context, each table you care about is exposed as a DbSet<T>. This lesson just uses it in its simplest form.

The diagram shows where EF Core sits. Your code writes LINQ. EF Core's pipeline turns that LINQ into a SQL query. A database-specific provider turns the SQL into whatever the actual database speaks on the wire. The same C# code, with a different provider package, runs against SQL Server, SQLite, PostgreSQL, or MySQL without changes to your queries.

Supported Database Providers

EF Core is provider-based. The core library knows how to track entities and build query trees, but it doesn't know how to talk to any specific database. That job belongs to a provider, a separate NuGet package that plugs into EF Core and handles the database-specific bits: connection management, SQL dialect, type mapping, and the wire protocol.

DatabaseProvider packageMaintained byNotes
SQL ServerMicrosoft.EntityFrameworkCore.SqlServerMicrosoftThe default in Microsoft documentation. Works with SQL Server and Azure SQL.
SQLiteMicrosoft.EntityFrameworkCore.SqliteMicrosoftFile-based, zero-config. Perfect for samples and small apps.
PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQLNpgsql teamThe standard Postgres provider. Very actively maintained.
MySQL / MariaDBPomelo.EntityFrameworkCore.MySqlPomelo teamMost popular community MySQL provider.
In-memoryMicrosoft.EntityFrameworkCore.InMemoryMicrosoftFor tests only. Not a real database.
OracleOracle.EntityFrameworkCoreOracleProvided by Oracle, not Microsoft.
Cosmos DBMicrosoft.EntityFrameworkCore.CosmosMicrosoftDocument-store flavor of EF Core; not relational.

A few things worth knowing about the provider model:

The provider is what determines which SQL dialect EF Core generates. The same db.Products.Where(p => p.Name.StartsWith("Wireless")) query becomes WHERE Name LIKE 'Wireless%' on SQL Server, WHERE "Name" LIKE 'Wireless%' on PostgreSQL (Postgres uses double quotes for identifiers), and roughly the same shape on SQLite and MySQL. You write the LINQ once; the provider handles the dialect.

Not every LINQ query translates to every database. SQL Server supports certain function calls that PostgreSQL doesn't, and vice versa. EF Core will throw a clear error at runtime if you write a query the provider can't translate, telling you which call is the problem.

The in-memory provider exists for unit tests, not for "I don't want to set up a database." It has different behavior than real providers (no real transactions, no referential integrity), and code that works against it can fail against a real database. For learning and samples, use SQLite instead. That's exactly what this lesson does.

Installing the NuGet Packages

EF Core comes as a set of NuGet packages, not a single monolithic library. You install the pieces you need.

For a basic console app talking to SQLite, three packages cover almost everything:

What each one does:

  • `Microsoft.EntityFrameworkCore` is the core library: DbContext, DbSet<T>, the change tracker, LINQ query infrastructure. Every EF Core app references this directly or transitively. In practice, you usually don't have to install it explicitly because each provider package depends on it.
  • `Microsoft.EntityFrameworkCore.Sqlite` is the SQLite provider. It pulls in Microsoft.EntityFrameworkCore as a dependency and adds the SQLite-specific code that translates queries and talks to SQLite. For SQL Server, you'd swap this for Microsoft.EntityFrameworkCore.SqlServer. For PostgreSQL, Npgsql.EntityFrameworkCore.PostgreSQL. The rest of your code stays the same.
  • `Microsoft.EntityFrameworkCore.Tools` adds the design-time commands (dotnet ef migrations add, dotnet ef database update) you use to manage schema changes. You don't strictly need this for the "hello world" example below, but it's the package every real project installs, so it's worth adding from the start.

You also need the dotnet-ef command-line tool installed globally to run those design-time commands from the terminal:

That's a one-time install per machine, not per project. Once it's there, dotnet ef migrations add InitialCreate works in any EF Core project.

The Smallest End-to-End Example

Time to make this concrete. The full example below is a complete EF Core program: one entity, one DbContext, a database that gets created on first run, an insert, and a query. It uses SQLite so you don't need to install or configure anything beyond .NET 8 and the three NuGet packages above.

First, the entity. EF Core calls a class that maps to a table an entity. There's nothing special about it: just a normal C# class with public properties.

A property named Id (or <ClassName>Id, so ProductId would also work) is treated by convention as the primary key. The int type makes it an auto-incrementing integer key. EF Core will figure all of this out without any annotations or configuration. The _Code-First Approach_ lesson covers exactly how this convention works and how to override it.

Next, the DbContext. This is the class that represents your database session. You derive from Microsoft.EntityFrameworkCore.DbContext, declare a DbSet<T> for each entity type, and override OnConfiguring to tell EF Core which provider and connection string to use.

DbSet<Product> is the property you use to read and write products. UseSqlite is the extension method the SQLite provider adds to DbContextOptionsBuilder, and "Data Source=shop.db" is a SQLite connection string that points to a file named shop.db in the working directory. The file gets created automatically on first use.

Production apps usually configure the context through dependency injection in Program.cs instead of overriding OnConfiguring. Both work. For a quick sample, overriding OnConfiguring is the shortest path.

Now the program itself, using top-level statements:

Output (first run):

Walking through what each line does:

using var db = new ShopContext() creates the context. The using ensures it gets disposed at the end of the scope, which closes the underlying database connection. Treating a context as short-lived is the standard pattern.

await db.Database.EnsureCreatedAsync() looks at the current model (one entity, Product) and creates the database file and the Products table if they don't already exist. This method is the simplest way to bring up a schema for a sample or test. It's not how you manage schema in a real app. Real apps use migrations. The reason: EnsureCreated does nothing if the database already exists, so it can't apply schema changes once you start evolving your model.

The if (!db.Products.Any()) guard prevents duplicate seeding on repeated runs. Any() translates to a SELECT EXISTS (SELECT 1 FROM Products) SQL query, so it's cheap.

db.Products.AddRange(...) queues three new entities for insertion. Nothing has hit the database yet; EF Core is just tracking the new objects in memory. await db.SaveChangesAsync() is what actually generates and runs the INSERT statements. After the call, each Product's Id is set to the value the database picked, because SQLite's INTEGER PRIMARY KEY is auto-incremented and EF Core reads the generated value back.

The foreach (Product product in db.Products) loop translates to a SELECT Id, Name, Price, Stock FROM Products query. EF Core hydrates each row into a Product object and yields it to the loop. The objects you get back are fully-typed C# objects with the right properties; no manual mapping, no readers, no parameter binding.

The whole program is about 20 lines of code and replaces a couple of hundred lines of ADO.NET boilerplate. That's the value proposition of an ORM in one example.

EF Core vs Raw ADO.NET

The boilerplate gap between EF Core and raw ADO.NET is the main reason most teams reach for an ORM. A side-by-side puts the trade-offs in one place.

AspectEF CoreRaw ADO.NET
Query languageLINQ over typed entitiesSQL strings, parameters bound by name or index
Result mappingAutomatic, into your C# classesManual, column-by-column, type-by-type
Change trackingBuilt in; modify objects, call SaveChangesManual; you write the UPDATE and pass new values
Schema managementMigrations (dotnet ef migrations add)Manual scripts (or a separate tool)
Connection managementHandled by the contextYou open, close, and dispose connections
Type safetyCompile-time checking on queriesSQL strings are checked only at runtime
Learning curveHigher up-front (concepts, conventions)Lower up-front, but more code per query
PerformanceSlightly more overhead per queryBare-metal; nothing between you and the wire
When it shinesCRUD-heavy apps, line-of-business logicHot paths, complex reports, bulk operations

A few notes on that "performance" row. EF Core adds maybe 5-20% per-query overhead compared to hand-written ADO.NET for the same SQL, depending on the workload. For most apps, that overhead is invisible against network latency to the database. For a handful of hot queries in a high-throughput service, it's a real cost worth measuring. The standard pattern at scale is "EF Core for 95% of the app, Dapper or raw ADO.NET for the 5% that need it."

The "type safety" row is bigger than it looks. With EF Core, renaming a column means renaming a C# property and getting compile errors everywhere the old name was used. With raw SQL strings, the same rename means hunting through every query string by hand and finding out at runtime that you missed one. The compiler catching SQL mistakes is one of the unsung wins of an ORM.

When Not to Use EF Core

EF Core isn't always the right tool. Three cases where reaching for something else is the cleaner choice:

Heavy reporting and analytics queries. When you're writing a query with seven joins, three subqueries, and a window function, expressing it in LINQ is harder than just writing the SQL. EF Core can run raw SQL through FromSqlInterpolated and SqlQuery<T>, but at that point you're using EF Core as a row mapper, not as a query builder. Dapper is purpose-built for that scenario: you write the SQL, Dapper hydrates the result into your C# types, and you skip the LINQ-to-SQL translation entirely.

Bulk operations. Inserting or updating tens of thousands of rows through EF Core's change tracker is slow, because the tracker is doing per-row work. EF Core 7 added ExecuteUpdate and ExecuteDelete for bulk updates and deletes that bypass the tracker, but for very large inserts, the standard pattern is SqlBulkCopy (SQL Server) or COPY (PostgreSQL) through provider-specific APIs.

Tight performance budgets. A service handling thousands of requests per second where every millisecond of CPU matters can save real money by hand-tuning SQL and using a lighter mapper. That's a small fraction of apps, but it exists.

The pattern most production teams use is: start with EF Core for everything because it's productive and safe, profile when something's slow, and drop down to Dapper or raw ADO.NET for the specific operations that need it. You don't have to pick one tool for the whole codebase. EF Core and Dapper can share a database connection and run in the same app side by side.

A Few Things to Keep in Mind

Before moving on, three points that catch people on their first EF Core project.

EF Core is not magic, it's just SQL underneath. Every query you write in LINQ becomes a SQL statement that runs on the database. If your query is slow, the fix is usually at the SQL level: an index, a different join, or a smaller result set. EF Core lets you log the generated SQL through LogTo so you can see exactly what it's sending.

The DbContext is meant to be short-lived. A context is roughly the EF Core equivalent of a database connection plus a unit of work. The standard pattern in a web app is one context per request: created at the start, disposed at the end. Long-lived contexts accumulate tracked entities in memory and eventually slow down.

Conventions do a lot of the work, but you can override anything. EF Core ships with sensible defaults: a property named Id is the primary key, a string property maps to a nullable text column, foreign keys are inferred from navigation properties. When the convention doesn't fit your schema, you override it with data annotations (attributes on properties) or the Fluent API (configuration in OnModelCreating).

That's enough for the introduction. You've seen what an ORM is, what EF Core is, how it sits between your code and the database, which providers exist, how to install it, and what a complete program looks like end to end.

Summary

  • An ORM (Object-Relational Mapper) bridges relational tables and rows with C# classes and objects, so you work in code instead of writing SQL by hand. EF Core is Microsoft's modern ORM for .NET, with EF Core 8 targeting .NET 8 as the current major version.
  • EF Core does four main things: models data through plain C# entity classes, tracks in-memory changes for automatic INSERT/UPDATE/DELETE generation, translates LINQ queries into provider-specific SQL, and manages schema evolution through migrations.
  • The piece that holds it together is DbContext, which represents a session with the database. Each table you care about is exposed as a DbSet<T> on the context.
  • EF Core is provider-based. The core library is database-agnostic; a separate NuGet package per database (SQL Server, SQLite, PostgreSQL, MySQL, and others) plugs in to handle dialect, type mapping, and the wire protocol. The same LINQ queries work across all of them.
  • A minimal setup is three NuGet packages (Microsoft.EntityFrameworkCore, the provider package, and Microsoft.EntityFrameworkCore.Tools) plus the global dotnet-ef CLI tool. SQLite is the simplest provider for samples since it's a file-based database with zero configuration.
  • The EnsureCreatedAsync method creates a database and schema if they don't exist. It's fine for samples and tests but not for production, where migrations handle incremental schema changes over time.
  • EF Core fits most CRUD-heavy line-of-business code well. For heavy reporting queries, bulk operations, and hot performance paths, Dapper or provider-specific APIs (like SqlBulkCopy) are the standard alternatives. Most teams use EF Core for the bulk of the app and drop down where it pays off.

The _DbContext & DbSet_ lesson goes deep on the two types you saw briefly here. It covers context lifetime (why short-lived contexts are the norm), dependency injection setup with AddDbContext, configuration through DbContextOptions, the role of DbSet<T> as both a queryable source and an entity collection, and the change tracker that ties saves to in-memory edits.