.NET Academy

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 / UseSqlite

Querying

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);
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.sql

Concurrency

[Timestamp] public byte[] RowVersion { get; set; } = [];   // SQL Server rowversion
// SaveChanges → DbUpdateConcurrencyException if the row changed since it was read

Performance checklist

  • Log SQL in development: "Microsoft.EntityFrameworkCore.Database.Command": "Information".
  • No N+1: Include or 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.
  • DbContext is scoped (one per request) and not thread-safe.