.NET Academy

Entity Framework Core basics

Map C# classes to tables, query with LINQ, save changes, and evolve the schema with migrations.

14 min read

You will learn

  • Define entities and a DbContext
  • Query with LINQ and understand when SQL is executed
  • Insert, update and delete with change tracking and SaveChanges
  • Create and apply migrations

An ORM for .NET

Entity Framework Core is .NET's object-relational mapper (ORM). You work with C# classes and LINQ; EF Core translates them to SQL, executes it, and maps rows back to objects. It supports SQL Server, PostgreSQL, SQLite, MySQL, Cosmos DB and more.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef

Entities and the DbContext

Entities are plain classes; the DbContext describes which ones are stored:

public class Product
{
    public int Id { get; set; }                       // primary key by convention
    public required string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}
 
public class ShopDb(DbContextOptions<ShopDb> options) : DbContext(options)
{
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
 
    protected override void OnModelCreating(ModelBuilder model)
    {
        model.Entity<Product>().Property(p => p.Name).HasMaxLength(200);
        model.Entity<Product>().Property(p => p.Price).HasPrecision(10, 2);
        model.Entity<Product>().HasIndex(p => p.Name);
    }
}

Conventions do most of the mapping: Id becomes the primary key, string becomes text, int? becomes a nullable column. Configure the rest with the fluent API (as above) or attributes.

Register it in Program.cs — it's scoped, one per request:

builder.Services.AddDbContext<ShopDb>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Shop")));

Querying with LINQ

Everything from Track 4 applies — but now the query becomes SQL that runs in the database. Explore what EF Core generates:

Interactive

LINQ → SQL

What Entity Framework Core actually sends to the database (SQL Server)

C# (LINQ)

var cheap = await db.Products
    .Where(p => p.Price < 20)
    .OrderBy(p => p.Name)
    .ToListAsync();

Generated SQL1 query sent

SELECT [p].[Id], [p].[Name], [p].[Price], [p].[Stock]
FROM [Products] AS [p]
WHERE [p].[Price] < 20.0
ORDER BY [p].[Name]

Rows returned

IdNamePriceStock
2Mouse19.9910
3Notebook4.5100
4Pen1.20
Where becomes WHERE and OrderBy becomes ORDER BY. The filtering happens in the database — only matching rows travel over the network.
var cheap = await db.Products
    .Where(p => p.Price < 20)
    .OrderBy(p => p.Name)
    .Select(p => new ProductDto(p.Id, p.Name, p.Price))   // fetch only needed columns
    .ToListAsync();
 
var product = await db.Products.FindAsync(id);           // by primary key (checks tracked entities first)
bool any = await db.Products.AnyAsync(p => p.Stock == 0);
  • Nothing runs until you enumerate (ToListAsync, FirstOrDefaultAsync, CountAsync…).
  • Use the async methods in web apps.
  • Only expressions EF Core can translate work; calling your own C# methods inside a query fails (see "Can't translate" above).

Saving data: change tracking

Entities loaded through a DbContext are tracked. Modify them, then call SaveChangesAsync — EF Core works out the SQL:

// Create
db.Products.Add(new Product { Name = "Lamp", Price = 39.99m, Stock = 12 });
 
// Update
var desk = await db.Products.SingleAsync(p => p.Name == "Desk");
desk.Price = 229m;                          // the change tracker notices
 
// Delete
var pen = await db.Products.FindAsync(6);
if (pen is not null) db.Products.Remove(pen);
 
int changed = await db.SaveChangesAsync();  // one transaction: INSERT + UPDATE + DELETE

SaveChangesAsync wraps all pending changes in a transaction. For bulk operations without loading entities, use ExecuteUpdateAsync / ExecuteDeleteAsync:

await db.Products.Where(p => p.Stock == 0).ExecuteDeleteAsync();

Migrations: evolving the schema

Your model changes over time — a new property, a new table. Migrations turn model changes into versioned schema changes:

dotnet ef migrations add InitialCreate        # generates C# describing the schema
dotnet ef database update                     # applies pending migrations to the database
dotnet ef migrations add AddProductStock      # after adding a property
dotnet ef migrations script --idempotent      # SQL script for production deployments

Migrations are code in your repository, reviewed and versioned like everything else.

Read the SQL

Turn on SQL logging in development ("Microsoft.EntityFrameworkCore.Database.Command": "Information" in the logging config) and look at what EF Core sends. It's the fastest way to spot slow or chatty queries.

Key takeaways

  • Entities are classes; a DbContext with DbSet<T>s maps them to tables (conventions + fluent API).
  • LINQ queries become SQL when enumerated; use async methods and projections.
  • Tracked entities + SaveChangesAsync = automatic INSERT/UPDATE/DELETE in one transaction.
  • Migrations version your schema: dotnet ef migrations add, dotnet ef database update.

Quiz

4 questions · 3 correct to pass
  1. 1.What does a DbContext represent?

  2. 2.When does `db.Products.Where(p => p.Price < 20)` send SQL to the database?

  3. 3.You load a product, change its Price, and call SaveChangesAsync. What happens?

  4. 4.What is a migration?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)