ASP.NET Core MVC · Data Access

Entity Framework Core

Entity Framework Core (EF Core) is .NET’s object-relational mapper. You work with C# classes and LINQ; EF Core writes the SQL, runs it on SQL Server (or PostgreSQL, SQLite…), and turns rows back into objects. ShopSmart stores its catalogue with it.

Intermediate 7 min Lesson 21 of 42Examples target .NET 9.
0/42 in ASP.NET Core MVC
On this page · 7 min · Intermediate

In simple words#

EF Core is a translator: you speak C# (LINQ on objects), it speaks SQL to the database and brings back objects. You rarely write SQL yourself.

DbContext
Your session with the database.
DbSet<T>
A table as a collection.
SaveChanges
Writes tracked changes to the database.

Learning objectives#

  • Describe entities, DbContext and DbSet
  • Use conventions for keys, foreign keys and relationships
  • Insert, query and update data
  • See the SQL EF Core generates

Explanation#

An entity is a plain class (Product, Category). A DbContext represents a session with the database; each DbSet<T> property is a table you can query.

Conventions remove boilerplate: a property named Id is the primary key, CategoryId plus a Category navigation property form a foreign key, and a List<Product> on Category makes it one-to-many. Attributes ([MaxLength], [Precision]) or the Fluent API fine-tune columns.

Insert by adding objects and calling SaveChangesAsync() — EF Core sends all inserts in one transaction and fills in generated Ids.

Queries are LINQ: Where, OrderBy, Select, Include. Nothing runs until you enumerate (ToListAsync, FirstAsync…). ToQueryString() shows the SQL — great for learning and debugging.

Change tracking: entities loaded from a context are tracked. Change a property and SaveChangesAsync() writes an UPDATE for just that column. Use AsNoTracking() for read-only queries to save memory and time.

Syntax#

C# Illustrative
db.Products.Add(new Product { Name = "Mouse", Price = 799m });
await db.SaveChangesAsync();                                     // INSERT


var cheap = await db.Products.Where(p => p.Price < 1000).ToListAsync();   // SELECT


product.Price = 749m;
await db.SaveChangesAsync();                                     // UPDATE

Code example: Insert, query and update with EF Core and SQL Server#

C#Program.cs Illustrative
// Program.cs
using Microsoft.EntityFrameworkCore;


var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ShopSmart_EfIntro;Trusted_Connection=True;TrustServerCertificate=True")
    .Options;


await using var db = new AppDbContext(options);
await db.Database.EnsureDeletedAsync();             // demo only: start with an empty database
await db.Database.EnsureCreatedAsync();             // create the tables from the model


// INSERT: add objects, then save once
var audio = new Category { Name = "Audio" };
db.Products.AddRange(
    new Product { Name = "Wireless Earbuds", Price = 2499m, Stock = 15, Category = audio },
    new Product { Name = "Bluetooth Speaker", Price = 3299m, Stock = 4, Category = audio },
    new Product { Name = "USB-C Hub", Price = 1499m, Stock = 30, Category = new Category { Name = "Accessories" } });
Console.WriteLine(
quot;Rows inserted:
{await db.SaveChangesAsync()}");
// QUERY: LINQ is translated to SQL var cheap = db.Products .Where(p => p.Price < 3000) .OrderBy(p => p.Name) .Select(p => new { p.Name, p.Price, Category = p.Category!.Name }); Console.WriteLine(cheap.ToQueryString()); foreach (var item in await cheap.ToListAsync()) Console.WriteLine(
quot;
{item.Name} ({item.Category}) Rs. {item.Price}");
// UPDATE: change a tracked entity, then save var speaker = await db.Products.SingleAsync(p => p.Name == "Bluetooth Speaker"); speaker.Stock -= 1; Console.WriteLine(
quot;Rows updated:
{await db.SaveChangesAsync()}, stock now {speaker.Stock}");
await db.Database.EnsureDeletedAsync(); // clean up the demo database // Data/AppDbContext.cs using Microsoft.EntityFrameworkCore; public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) { public DbSet<Product> Products => Set<Product>(); public DbSet<Category> Categories => Set<Category>(); } // Models/Product.cs using System.ComponentModel.DataAnnotations; using Microsoft.EntityFrameworkCore; public class Product { public int Id { get; set; } // primary key by convention [MaxLength(100)] public string Name { get; set; } = ""; [Precision(10, 2)] public decimal Price { get; set; } public int Stock { get; set; } public int? CategoryId { get; set; } // foreign key by convention (optional) public Category? Category { get; set; } // navigation property } public class Category { public int Id { get; set; } [MaxLength(50)] public string Name { get; set; } = ""; public List<Product> Products { get; set; } = []; }
Expected output
Rows inserted: 5
SELECT [p].[Name], [p].[Price], [c].[Name] AS [Category]
FROM [Products] AS [p]
LEFT JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id]
WHERE [p].[Price] < 3000.0
ORDER BY [p].[Name]
  USB-C Hub (Accessories) Rs. 1499.00
  Wireless Earbuds (Audio) Rs. 2499.00
Rows updated: 1, stock now 3

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. EnsureCreatedAsync builds Products and Categories tables from the classes (real apps use migrations instead).
  2. Adding three products saves five rows: EF Core also inserts the two new categories and links the foreign keys.
  3. The LINQ query becomes one SELECT with a JOIN (LEFT, because CategoryId is optional), a WHERE and an ORDER BY — ToQueryString prints it.
  4. Projecting with Select loads only the three needed columns.
  5. Changing Stock on a tracked entity produces a one-row UPDATE when SaveChangesAsync runs.

Real-world use case#

ShopSmart’s catalogue, carts, orders and customers live in SQL Server through one AppDbContext. Controllers and services never write SQL by hand except for a few reports.

Common mistakes#

Loading everything then filtering in memory

db.Products.ToList().Where(…) downloads the whole table. Keep Where before ToListAsync so it runs in SQL.

Forgetting decimal precision

Without [Precision] or HasPrecision, EF Core warns and uses decimal(18,2); be explicit for money.

N+1 queries

Accessing product.Category inside a loop without Include or a projection can run one query per item. Use Include or Select.

Best practices#

  • Async methods (ToListAsync, SaveChangesAsync)
  • AsNoTracking or projections for read-only pages
  • One DbContext per request (scoped)
  • Migrations, not EnsureCreated, for real databases

Remember this#

DbContext = session. DbSet<T> = table. Entity = row.

Nothing runs until ToListAsync / FirstAsync / SaveChangesAsync.

Tracked entity + SaveChanges = UPDATE.

Practice exercise#

Your task: Write a query that returns each category name with its number of products and the average price, ordered by name.

Need a hint?
db.Categories.Select(c => new { c.Name, Count = c.Products.Count, Average = c.Products.Average(p => p.Price) }).
Show solution
C# Illustrative
// Program.cs
using Microsoft.EntityFrameworkCore;


var options = new DbContextOptionsBuilder<AppDbContext>().UseInMemoryDatabase("Shop").Options;
await using var db = new AppDbContext(options);


var summary = await db.Categories
    .OrderBy(c => c.Name)
    .Select(c => new
    {
        c.Name,
        Count = c.Products.Count,
        Average = c.Products.Select(p => (decimal?)p.Price).Average() ?? 0,
    })
    .ToListAsync();


foreach (var row in summary) Console.WriteLine(
quot;
{row.Name}: {row.Count} products, average {row.Average:N0}");

Mini quiz#

Entity Framework Core quizMultiple choice Question 1 of 3

What does a DbSet<Product> represent?

Score so far: 0/0

Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.