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.
On this page · 7 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
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#
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#
// 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; } = [];
}
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#
- EnsureCreatedAsync builds Products and Categories tables from the classes (real apps use migrations instead).
- Adding three products saves five rows: EF Core also inserts the two new categories and links the foreign keys.
- The LINQ query becomes one SELECT with a JOIN (LEFT, because CategoryId is optional), a WHERE and an ORDER BY — ToQueryString prints it.
- Projecting with Select loads only the three needed columns.
- 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#
db.Products.ToList().Where(…) downloads the whole table. Keep Where before ToListAsync so it runs in SQL.
Without [Precision] or HasPrecision, EF Core warns and uses decimal(18,2); be explicit for money.
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?
Show solution
// 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#
What does a DbSet<Product> represent?
Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions
My notes#
Saved in this browser.