Querying and Saving with EF Core
Query a DbSet with LINQ, finished by an async method such as ToListAsync, and EF Core runs it as SQL. Add, change or remove entities, then call SaveChangesAsync, and EF Core writes the INSERT, UPDATE or DELETE for you.
Journeyman · C# · Entity Framework Core · Updated
After this lesson you can
- Query a table with LINQ through a DbSet, and see the SQL that EF Core writes
- Add, change and remove rows with Add, Remove and SaveChangesAsync
Key takeaways
- A LINQ chain on a
DbSetis anIQueryable<T>: a query waiting to run, which EF Core turns into one SQL statement. - An async method such as
ToListAsyncorFirstOrDefaultAsyncruns the query;ToQueryString()shows its SQL without running it. Include(c => c.Products)loads related rows. Without it, a category'sProductslist stays empty.Add,Removeand changed properties reach the database only when you callSaveChangesAsync(), which saves them all in one transaction.- Use
AsNoTracking()for read-only queries: it's faster for data you'll only display.
Every example in this lesson uses the Product, Category and NorthwindContext classes from DbContext and Entities, and a context named db.
Querying
Query a DbSet with the LINQ methods you know, and finish with the async version of the last step: ToListAsync, FirstOrDefaultAsync, CountAsync, AnyAsync. They come from Microsoft.EntityFrameworkCore.
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
DbContextOptions<NorthwindContext> options = new DbContextOptionsBuilder<NorthwindContext>().UseSqlServer(connectionString).Options;
using NorthwindContext db = new NorthwindContext(options);
decimal maxPrice = 8m;
IQueryable<Product> query = db.Products
.Where(p => p.UnitPrice < maxPrice)
.OrderBy(p => p.ProductName);
Console.WriteLine(query.ToQueryString());
Console.WriteLine();
List<Product> cheap = await query.ToListAsync();
foreach (Product product in cheap)
{
Console.WriteLine( quot;{product.ProductName} {product.UnitPrice:C}");
}
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public int? CategoryId { get; set; }
[Column(TypeName = "money")]
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public bool Discontinued { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; } = "";
public string? Description { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
}
public class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options) : base(options)
{
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
}
DECLARE @maxPrice money = 8.0;
SELECT [p].[ProductId], [p].[CategoryId], [p].[Discontinued], [p].[ProductName], [p].[UnitPrice], [p].[UnitsInStock]
FROM [Products] AS [p]
WHERE [p].[UnitPrice] < @maxPrice
ORDER BY [p].[ProductName]
Filo Mix $7.00
Geitost $2.50
Guaraná Fantástica $4.50
Konbu $6.00
Rhönbräu Klosterbier $7.75
Tourtière $7.45
- The chain is a query, not a list.
db.Products.Where(...).OrderBy(...)is anIQueryable<Product>: a description of a query, waiting to be run. It's the deferred execution from What is LINQ?, taken further: instead of looping over a list, EF Core turns the whole description into SQL. ToQueryString()shows that SQL without running it. The variablemaxPricebecame a parameter,@maxPrice, so the query is safe from SQL injection, just like Dapper's@parameters.ToListAsync()runs it: one query, one trip to the database.
More ways to read:
| Call | Runs |
|---|---|
await db.Products.FindAsync(1) |
Finds by primary key; null if there's none |
await db.Products.FirstOrDefaultAsync(p => p.ProductName == "Chai") |
The first match, or null |
await db.Products.CountAsync(p => p.CategoryId == 1) |
SELECT COUNT(*) ... WHERE |
await db.Categories.Include(c => c.Products).FirstAsync(c => c.CategoryId == 8) |
The category and its products, in one query with a join |
Without Include, a category's Products list stays empty: EF Core only loads related rows when you ask.
Saving
To change data, change the objects, then call SaveChangesAsync(). The context remembers every entity it loaded or was given (this is called change tracking), works out what's different, and writes the SQL:
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
DbContextOptions<NorthwindContext> options = new DbContextOptionsBuilder<NorthwindContext>().UseSqlServer(connectionString).Options;
using NorthwindContext db = new NorthwindContext(options);
// Add: INSERT
Product snack = new Product { ProductName = "Trail Mix", UnitPrice = 4.50m, UnitsInStock = 20, CategoryId = 3 };
db.Products.Add(snack);
await db.SaveChangesAsync();
Console.WriteLine( quot;Added with a new id: {snack.ProductId > 77}");
// Change: UPDATE
snack.UnitPrice = 5.00m;
await db.SaveChangesAsync();
Product? saved = await db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.ProductId == snack.ProductId);
Console.WriteLine( quot;New price: {saved?.UnitPrice:C}");
// Remove: DELETE
db.Products.Remove(snack);
await db.SaveChangesAsync();
Console.WriteLine( quot;Still there: {await db.Products.AnyAsync(p => p.ProductName == "Trail Mix")}");
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public int? CategoryId { get; set; }
[Column(TypeName = "money")]
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public bool Discontinued { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; } = "";
public string? Description { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
}
public class NorthwindContext : DbContext
{
public NorthwindContext(DbContextOptions<NorthwindContext> options) : base(options)
{
}
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
}
Added with a new id: True
New price: $5.00
Still there: False
Addmarks a new entity to be inserted. AfterSaveChangesAsync(), itsProductIdholds the id the database chose: noOUTPUT INSERTEDneeded.- Changing a property of a tracked entity is enough:
SaveChangesAsync()notices and runs anUPDATEof just that column. Removemarks an entity to be deleted.- Nothing reaches the database until
SaveChangesAsync(). Several changes saved together are sent in one transaction: all of them happen, or none do. AsNoTracking()reads without tracking, which is faster for data you'll only display. Use it for read-only queries.
In a controller
Put it together, and a controller needs no DAO at all:
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
Product? product = await db.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return product;
}
[HttpPost]
public async Task<ActionResult<Product>> Add(Product product)
{
db.Products.Add(product);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = product.ProductId }, product);
}
Check yourself
2 questions to try on the lesson page.
So what
With EF Core, reading is LINQ and writing is changing objects. Finish queries with an async method, call SaveChangesAsync() when you're done changing, and let EF Core write the SQL in between.
Key terms
- LINQ Query
- IQueryable<T>
- Change Tracking
- SaveChanges
- Include
- AsNoTracking