C# Entity Framework Cheat Sheet
Covers Entity Framework Core basics: DbContext, DbSet, CRUD operations, eager loading with Include, and managing schema migrations from the CLI.
DbContext & Model
Defining entities and the database session type.
public class Blog{ public int Id { get; set; } public string Title { get; set; } = string.Empty; public List<Post> Posts { get; set; } = new();}public class AppDbContext : DbContext{ public DbSet<Blog> Blogs => Set<Blog>(); public DbSet<Post> Posts => Set<Post>(); protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("Server=.;Database=BlogDb;Trusted_Connection=True;");}
CRUD Operations
Creating, reading, updating, and deleting entities.
using var db = new AppDbContext();// Createdb.Blogs.Add(new Blog { Title = "My Blog" });await db.SaveChangesAsync();// Readvar blog = await db.Blogs.FirstOrDefaultAsync(b => b.Title == "My Blog");var all = await db.Blogs.Include(b => b.Posts).ToListAsync(); // Eager load related data// Updateblog!.Title = "Updated Title";await db.SaveChangesAsync();// Deletedb.Blogs.Remove(blog);await db.SaveChangesAsync();
Migrations (CLI)
Versioning the database schema from your model.
dotnet tool install --global dotnet-ef # Install the EF Core CLI tooldotnet ef migrations add InitialCreate # Generate a migration from model changesdotnet ef database update # Apply pending migrations to the databasedotnet ef migrations remove # Remove the last unapplied migration
Key Concepts
Core EF Core building blocks.
- DbContext- Represents a session with the database; tracks entity changes for SaveChanges()
- DbSet<T>- Represents a table/collection queried and modified via the context
- Change tracking- EF tracks entity state (Added, Modified, Deleted, Unchanged) automatically
- Include / ThenInclude- Eagerly load related navigation properties in a single query
- Lazy loading- Related data loaded on first access when navigation properties are virtual and enabled
- Migrations- Code-first schema versioning generated from model changes
- AsNoTracking()- Skips change tracking for read-only queries, improving performance
Fluent API Configuration
Configuring relationships, indexes, and constraints beyond what data annotations express.
public class AppDbContext : DbContext{ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>(b => { b.HasIndex(x => x.Title).IsUnique(); b.Property(x => x.Title).HasMaxLength(200).IsRequired(); b.HasMany(x => x.Posts) .WithOne(p => p.Blog) .HasForeignKey(p => p.BlogId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity<Post>() .Property(p => p.Status) .HasConversion<string>(); // Store enum as string instead of int }}
Raw SQL Queries
Dropping to SQL for complex queries or bulk statements while staying injection-safe.
// Parameterized via string interpolation (EF escapes the values automatically)var recentBlogs = await db.Blogs .FromSqlInterpolated($"SELECT * FROM Blogs WHERE CreatedAt > {cutoff}") .ToListAsync();// Composable: LINQ can still filter/sort a FromSql resultvar filtered = await db.Blogs .FromSqlInterpolated($"SELECT * FROM Blogs") .Where(b => b.Posts.Count > 0) .ToListAsync();// Non-query statements (updates, deletes, stored procs)int rows = await db.Database.ExecuteSqlInterpolatedAsync( $"UPDATE Posts SET Archived = 1 WHERE BlogId = {blogId}");
Optimistic Concurrency & Transactions
Detecting conflicting updates and grouping multiple SaveChanges calls atomically.
public class Blog{ public int Id { get; set; } [Timestamp] public byte[] RowVersion { get; set; } = default!; // Concurrency token}try{ blog.Title = "New Title"; await db.SaveChangesAsync();}catch (DbUpdateConcurrencyException ex){ var dbValues = await ex.Entries.Single().GetDatabaseValuesAsync(); // Resolve: reload, merge, or reject the update}using var tx = await db.Database.BeginTransactionAsync();try{ db.Blogs.Add(new Blog { Title = "A" }); await db.SaveChangesAsync(); db.Posts.Add(new Post { Title = "B" }); await db.SaveChangesAsync(); await tx.CommitAsync();}catch { await tx.RollbackAsync(); throw; }
Compiled Queries & Projections
Cutting query-plan overhead and payload size for hot-path reads.
// Compiled query: skips LINQ expression-tree translation on every callprivate static readonly Func<AppDbContext, int, Task<Blog?>> GetBlogById = EF.CompileAsyncQuery((AppDbContext db, int id) => db.Blogs.FirstOrDefault(b => b.Id == id));var blog = await GetBlogById(db, 42);// Projection avoids loading full entities and their tracked graphvar summaries = await db.Blogs .Select(b => new BlogSummaryDto(b.Id, b.Title, b.Posts.Count)) .AsNoTracking() .ToListAsync();
Advanced EF Core Concepts
Patterns and pitfalls that matter once a project moves past basic CRUD.
- Shadow properties- Columns tracked by EF (e.g. foreign keys, audit timestamps) that don't exist as CLR properties on the entity
- Value converters- HasConversion() maps a CLR type to a different storage representation (enum-to-string, encrypted blob, etc.)
- Global query filters- HasQueryFilter() applies a predicate (e.g. soft-delete, multi-tenant) to every query against an entity automatically
- Owned types- OwnsOne()/OwnsMany() maps value objects (Address, Money) into the owner's table without a separate identity
- Interceptors- ISaveChangesInterceptor / IDbCommandInterceptor hook into EF's pipeline for auditing, soft-delete, or command logging
- Split queries- AsSplitQuery() issues multiple SQL queries for multiple Includes, avoiding the cartesian-explosion of a single JOIN
- Bulk operations- ExecuteUpdate()/ExecuteDelete() (EF Core 7+) run set-based updates/deletes without loading entities into memory
Use .AsNoTracking() on read-only queries such as API GET endpoints — it avoids the overhead of EF's change tracker and can noticeably speed up large result sets.