EF Core cheat sheet
DbContext setup, querying, saving, relationships and migrations.
Setup
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 b)
{
b.Entity<Product>().Property(p => p.Name).HasMaxLength(200).IsRequired();
b.Entity<Product>().Property(p => p.Price).HasPrecision(10, 2);
b.Entity<Product>().HasIndex(p => p.Sku).IsUnique();
b.Entity<Order>().HasMany(o => o.Lines).WithOne().OnDelete(DeleteBehavior.Cascade);
b.Entity<Order>().Property(o => o.Status).HasConversion<string>();
}
}
builder.Services.AddDbContext<ShopDb>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Shop"))); // or UseSqlServer / UseSqliteQuerying
var all = await db.Products.ToListAsync();
var one = await db.Products.FindAsync(id); // by key
var p = await db.Products.FirstOrDefaultAsync(p => p.Sku == sku);
var page = await db.Products.OrderBy(p => p.Id).Skip(20).Take(10).ToListAsync();
var dtos = await db.Products
.Where(p => p.Stock > 0)
.Select(p => new ProductDto(p.Id, p.Name, p.Price)) // projection
.ToListAsync();
var readOnly = await db.Products.AsNoTracking().ToListAsync();
bool exists = await db.Users.AnyAsync(u => u.Email == email);
int count = await db.Orders.CountAsync(o => o.Status == OrderStatus.Pending);Related data
await db.Orders.Include(o => o.Customer).ToListAsync(); // JOIN
await db.Orders.Include(o => o.Lines).ThenInclude(l => l.Product).ToListAsync();
await db.Orders.Include(o => o.Lines).AsSplitQuery().ToListAsync(); // avoid row explosion
await db.Orders.Select(o => new { o.Id, Customer = o.Customer.Name }).ToListAsync();Saving
db.Products.Add(product); // INSERT
product.Price = 9.99m; // UPDATE (tracked entity)
db.Products.Remove(product); // DELETE
await db.SaveChangesAsync(); // one transaction
await db.Products.Where(p => p.Stock == 0).ExecuteDeleteAsync(); // bulk
await db.Products.Where(p => p.Category == "x").ExecuteUpdateAsync(s => s.SetProperty(p => p.Price, p => p.Price * 0.9m));
await using var tx = await db.Database.BeginTransactionAsync(); // explicit transaction
...; await db.SaveChangesAsync(); await tx.CommitAsync();Raw SQL (parameterized)
var items = await db.Products.FromSql($"SELECT * FROM products WHERE price < {max}").ToListAsync();
await db.Database.ExecuteSqlAsync($"UPDATE products SET stock = 0 WHERE id = {id}");Interpolated values become parameters. Avoid FromSqlRaw with concatenated input.
Migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations script --idempotent -o deploy.sqlConcurrency
[Timestamp] public byte[] RowVersion { get; set; } = []; // SQL Server rowversion
// SaveChanges → DbUpdateConcurrencyException if the row changed since it was readPerformance checklist
- Log SQL in development:
"Microsoft.EntityFrameworkCore.Database.Command": "Information". - No N+1:
Includeor project; never query inside a loop. AsNoTracking()for reads; project only needed columns; paginate.- Index columns you filter and sort on; use async methods with a
CancellationToken. DbContextis scoped (one per request) and not thread-safe.