You've written the reader-loop that turns rows into objects a dozen times now. EF Core exists so you never have to write it again.
Go back and look at the ExecuteReader example from a few lessons ago. Notice the shape of it: open a connection, build a command, run it, then manually walk a DbDataReader row by row, pulling each column out by index or name and stuffing it into a new object — reader.GetInt32(0), reader.GetString(1), and so on.
Now imagine a real application with forty entity types — products, orders, customers, invoices, addresses — each with its own version of that same reader-loop, and each one needing to be kept in sync by hand every time a column is added, renamed, or its type changes. Multiply that by every query in the codebase and you have a genuinely large amount of repetitive, easy-to-get-subtly-wrong mapping code, none of which has anything to do with your actual business logic.
In this lesson, you'll learn what an ORM is, why Entity Framework Core exists to eliminate exactly this kind of boilerplate, how it relates to the ADO.NET you've already learned, and how to install and set it up in a project.
Entity Framework Core (EF Core) is Microsoft's ORM — object-relational mapper — for .NET. It lets you work with your database using ordinary C# classes and LINQ queries, instead of hand-writing SQL strings and manually mapping rows to objects. You describe your data as C# classes; EF Core translates operations on those classes into the SQL needed to read and write the underlying tables.
An ORM maps between two fundamentally different worlds: the relational model a database speaks (tables, rows, columns, foreign keys) and the object-oriented model your C# code speaks (classes, instances, properties, references). EF Core is that mapping layer for .NET: it turns rows into strongly-typed object instances on the way in, and turns changes to those objects into INSERT/UPDATE/DELETE statements on the way out — and it turns LINQ queries written against your C# classes into SELECT statements, as you'll see across this module.
Relational databases and object-oriented programs model the world differently, even when describing the exact same data. A table has rows and columns and knows nothing about inheritance, object references, or collections. A C# class can have properties that are other objects, lists of related objects, and behavior (methods) — none of which map cleanly, one-to-one, onto a table's structure. This gap has a name in software engineering: the object-relational impedance mismatch.
Every application working with a relational database from object-oriented code has to bridge that gap somehow. Done by hand with raw ADO.NET, bridging it means: writing the SQL, opening the connection, running the command, looping the reader, constructing an object per row, setting each property from the right column — for every single entity type, and again in reverse for every write. It's not intellectually hard, but it's a huge, repetitive amount of code that has nothing to do with what the application is actually supposed to do.
An ORM automates exactly that bridging work. You describe the shape of your data once, as C# classes (the subject of the next lesson), and the ORM handles translating queries and changes back and forth between the object world and the relational world — generating the reader-loops, the parameterized commands, and the property assignments that you'd otherwise write by hand, consistently, for every entity in your model.
EF Core doesn't replace ADO.NET or talk to the database over some entirely different protocol — it sits on top of ADO.NET and uses it to do the actual work.
This is worth remembering going forward: everything you learned earlier in this module is still happening underneath EF Core — connections are still pooled, commands are still parameterized, readers are still streamed. EF Core generates and drives that ADO.NET code for you; it doesn't remove it from the picture.
Here's the exact same "products low on stock" query you've already seen written twice with raw ADO.NET — once with ExecuteReader, once conceptually as a report. This is what it looks like with EF Core, once the entity and context are set up (both covered next lesson):
List<Product> lowStockProducts = await context.Products
.Where(p => p.Stock < 10)
.ToListAsync();
foreach (Product product in lowStockProducts)
{
Console.WriteLine($"Low stock: {product.Name} (Id {product.Id})");
}No connection object, no command, no reader loop, no manual column-to-property mapping. context.Products is a queryable collection of Product objects; .Where(...) is ordinary LINQ, exactly like the IEnumerable/IQueryable LINQ you learned in Intermediate Part IV. EF Core translates that LINQ expression into a parameterized SELECT ... WHERE Stock < @__p_0 behind the scenes — parameterized automatically, closing exactly the SQL injection risk from the parameters lesson, without you writing a single parameter yourself.
Picture an e-commerce backend with entities for Product, Order, OrderLine, and Customer — dozens of related tables in total. Written by hand with ADO.NET, every one of those entity types needs its own reader-loop for reads, its own parameterized INSERT/UPDATE logic for writes, and careful manual work anywhere one entity references another (loading a customer's orders means a second query, mapped and stitched together by hand).
With EF Core, the same team defines each entity once as a C# class, describes how they relate to each other (the "Relationships" lesson ahead), and from then on reads, writes, and navigation between related entities (order.Customer.Name, customer.Orders) are handled by the framework. This is exactly why most real-world .NET applications reach for EF Core rather than raw ADO.NET for their day-to-day data access — and exactly why understanding the ADO.NET underneath it, which you now do, makes EF Core's behavior far less mysterious.
Imagine two people who speak different languages trying to negotiate a contract. A skilled translator lets each side speak naturally in their own language — the negotiation still genuinely happens, the actual terms still get agreed on, nothing about the substance changes. The translator just removes the burden of manually converting every sentence yourself.
EF Core is that translator between your C# object model and the database's relational model. The database still runs real SQL, still enforces real constraints, still stores real rows in real tables — EF Core just removes the burden of manually writing the "sentences" (the SQL, the reader loops, the property mapping) that translate between the two, every single time.
EF Core ships as a set of NuGet packages, split into a core package and one package per database provider — you need both. For SQL Server:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design # design-time tools, needed for migrations
dotnet tool install --global dotnet-ef # the `dotnet ef` CLI, installed once per machine| Package | Role |
|---|---|
| Microsoft.EntityFrameworkCore | The core ORM engine — change tracking, LINQ translation, DbContext, DbSet. Pulled in automatically as a dependency of the provider package. |
| Microsoft.EntityFrameworkCore.SqlServer | The provider — translates EF Core's generic operations into SQL Server-specific SQL. Swappable for Npgsql.EntityFrameworkCore.PostgreSQL, Microsoft.EntityFrameworkCore.Sqlite, etc. without changing your entity classes or LINQ queries. |
| Microsoft.EntityFrameworkCore.Design | Design-time tooling that powers dotnet ef commands like generating migrations. |
| dotnet-ef | The command-line tool itself — dotnet ef migrations add, dotnet ef database update, and more, covered in the migrations lesson. |
The provider is the key architectural detail: your entity classes, your DbContext, and your LINQ queries stay almost entirely the same regardless of which database you're targeting — the provider is what translates EF Core's generic behavior into the specific SQL dialect (and specific feature set) of SQL Server, PostgreSQL, SQLite, or another supported engine.
Neither is true. EF Core is an application-layer library that generates and executes ADO.NET calls against a real relational database — the database engine, its constraints, its transaction guarantees, and ADO.NET underneath are all still fully present and doing real work. EF Core sits on top of ADO.NET; it doesn't replace it.
For everyday CRUD operations, that's largely true — and that's the whole point. But understanding roughly what SQL a given LINQ query produces still matters once queries get more complex, especially for performance: an innocent-looking LINQ query can occasionally translate into surprisingly expensive SQL. This module gives you the ADO.NET foundation specifically so that EF Core's behavior is something you can reason about, not a black box.
Adding only Microsoft.EntityFrameworkCore and expecting to connect to SQL Server. The core package has no idea how to talk to any specific database — that's entirely the provider's job. Always install a provider package (Microsoft.EntityFrameworkCore.SqlServer, etc.) — it pulls the core package in as a dependency automatically.
Running dotnet ef migrations add InitialCreate without Microsoft.EntityFrameworkCore.Design installed, or without the global dotnet-ef tool — this fails with a clear but sometimes confusing error the first time. Install both up front as part of project setup, before you need your first migration.
| Scenario | EF Core, or raw ADO.NET? |
|---|---|
| Typical CRUD-heavy application data access | EF Core — dramatically less boilerplate, and it's what most teams use day to day |
| A handful of related entities with straightforward relationships | EF Core — this is exactly its sweet spot |
| A single, hand-tuned reporting query where every millisecond and every generated query plan matters | Often still worth raw ADO.NET (or EF Core's raw-SQL escape hatches), for that specific query |
| A tiny script or tool that runs one or two queries and exits | Either is fine — raw ADO.NET avoids the setup overhead for something this small |
You've seen why EF Core exists and where it fits relative to ADO.NET. Let's check that the mental model is solid before moving on.
1. What problem is EF Core primarily designed to solve?
Correct: B
Why B is correct: EF Core exists to bridge the structural gap between the relational model (tables, rows) and the object-oriented model (classes, instances) — automating the mapping code developers would otherwise write by hand for every entity and every query.
Why A is incorrect: The database is still a real relational database underneath — EF Core doesn't replace it, it translates to and from it.
Why C is incorrect: EF Core isn't primarily a performance tool — it can sometimes be slower than expertly hand-tuned raw SQL for a specific query; its value is developer productivity and reduced boilerplate.
Why D is incorrect: A database connection is still required and still used — EF Core drives ADO.NET's connection and command objects underneath, exactly as you learned earlier in this module.
Reinforcement: EF Core's core value proposition is eliminating repetitive mapping code, not replacing the database or ADO.NET.
2. Which statement correctly describes the relationship between EF Core and ADO.NET?
Correct: B
Why B is correct: EF Core generates and drives ADO.NET calls under the hood — pooled connections, parameterized commands, and data readers are all still there, exactly as covered in the earlier lessons of this module; EF Core is a layer of automation on top, not a replacement.
Why A is incorrect: ADO.NET's connection, command, and reader classes are still very much in use — just driven by EF Core instead of by your own code directly.
Why C is incorrect: This has the layering backwards — ADO.NET is the lower-level foundation; EF Core is built on top of it, not the other way around.
Why D is incorrect: They aren't competitors — EF Core depends on and uses ADO.NET to do its actual database work.
Reinforcement: Everything you learned about connections, commands, parameters, and transactions is still happening underneath EF Core — it's just generated and driven automatically.
3. A team needs to switch their EF Core application from SQL Server to PostgreSQL. According to EF Core's architecture, what is the main change required?
Correct: B
Why B is correct: The provider is specifically the piece responsible for translating EF Core's generic operations into one database engine's SQL dialect. Swapping providers (and the connection string) is the primary change — your entity classes and LINQ queries are written against EF Core's abstractions, not against a specific database's syntax.
Why A is incorrect: This is exactly the manual, error-prone rewrite EF Core's provider model is designed to avoid — entities and LINQ queries don't need to be rewritten for a provider swap.
Why C is incorrect: While the core model largely transfers, providers can still differ in supported features and some SQL generation nuances — "zero differences of any kind" overstates it, but the point stands that no rewrite of the model is needed.
Why D is incorrect: The core package name doesn't change between databases — it's the separate provider package that's swapped.
Reinforcement: The provider is EF Core's designed swap point for targeting a different database engine — that's precisely why it's a separate package from the core.
You now understand what EF Core is, why it exists, and how it's installed. Next up: the object at the center of it all — DbContext.
dotnetmadeeasy.com — Learn C# and .NET, the right way.