📄

Efcore Cheatsheet

Intermediate 2 min read 300 words

Entity Framework Core Quick Reference Cheatsheet

Loading Strategies

Comparison Table

Strategy When Loaded SQL Queries Use When
Eager With main query 1 (or split) Always need related data
Explicit On demand N+1 possible Sometimes need data
Lazy On property access N+1 problem Rarely (avoid!)

Eager Loading

// Include related data
var orders = await _context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
    .ToListAsync();

// Split query (avoid cartesian explosion)
var orders = await _context.Orders
    .Include(o => o.Items)
    .AsSplitQuery()  // Separate query per include
    .ToListAsync();

Explicit Loading

var order = await _context.Orders.FindAsync(id);

// Load collection
await _context.Entry(order)
    .Collection(o => o.Items)
    .LoadAsync();

// Load reference
await _context.Entry(order)
    .Reference(o => o.Customer)
    .LoadAsync();

// With filter
await _context.Entry(order)
    .Collection(o => o.Items)
    .Query()
    .Where(i => i.Price > 100)
    .LoadAsync();

Query Optimization

No Tracking

// Single query
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

// Context-wide
_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

// With identity resolution (avoids duplicates)
var orders = await _context.Orders
    .Include(o => o.Customer)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

Compiled Queries

private static readonly Func<AppDbContext, int, Task<Product?>> _getProductById =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
        ctx.Products.FirstOrDefault(p => p.Id == id));

// Usage
var product = await _getProductById(_context, productId);

Projections

// Only select needed columns
var dtos = await _context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        CategoryName = p.Category.Name
    })
    .ToListAsync();

Global Query Filters

Setup

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Soft delete filter
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);

    // Multi-tenancy filter
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => o.TenantId == _tenantId);
}

// Bypass filter
var allProducts = await _context.Products
    .IgnoreQueryFilters()
    .ToListAsync();

Concurrency

Optimistic Concurrency

// In entity
public byte[] RowVersion { get; set; }

// Configuration
modelBuilder.Entity<Product>()
    .Property(p => p.RowVersion)
    .IsRowVersion();

// Handling conflicts
try
{
    await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    var entry = ex.Entries.Single();
    var dbValues = await entry.GetDatabaseValuesAsync();

    // Client wins
    entry.OriginalValues.SetValues(dbValues);
    await _context.SaveChangesAsync();

    // Database wins
    entry.Reload();
}

Advanced Modeling

Shadow Properties

// Configuration
modelBuilder.Entity<Product>()
    .Property<DateTime>("CreatedAt");

// Access
var createdAt = _context.Entry(product)
    .Property<DateTime>("CreatedAt").CurrentValue;

// In queries
var recent = await _context.Products
    .Where(p => EF.Property<DateTime>(p, "CreatedAt") > DateTime.UtcNow.AddDays(-7))
    .ToListAsync();

Owned Types

// Value object
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Configuration
modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.Address);

// JSON column (.NET 7+)
modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.Address, a => a.ToJson());

Inheritance (TPH/TPT/TPC)

// TPH (Table Per Hierarchy) - Default, single table
modelBuilder.Entity<Animal>()
    .HasDiscriminator<string>("AnimalType")
    .HasValue<Dog>("dog")
    .HasValue<Cat>("cat");

// TPT (Table Per Type) - Separate tables, JOINs
modelBuilder.Entity<Dog>().ToTable("Dogs");
modelBuilder.Entity<Cat>().ToTable("Cats");

// TPC (Table Per Concrete) - .NET 7+
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

Raw SQL

Safe Execution

// Parameterized query (safe!)
var products = await _context.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
    .ToListAsync();

// With LINQ composition
var expensive = await _context.Products
    .FromSqlRaw("SELECT * FROM Products")
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .ToListAsync();

// Non-query commands
await _context.Database.ExecuteSqlInterpolatedAsync(
    $"UPDATE Products SET Price = Price * {multiplier} WHERE CategoryId = {categoryId}");

Cascade Delete

Behavior Deletes Dependents Sets FK to Null
Cascade Yes N/A
SetNull No Yes
Restrict Exception N/A
NoAction Database decides Database decides
modelBuilder.Entity<Order>()
    .HasMany(o => o.Items)
    .WithOne(i => i.Order)
    .OnDelete(DeleteBehavior.Cascade);

Soft Delete Pattern

// Entity
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

// Global filter
foreach (var entityType in modelBuilder.Model.GetEntityTypes()
    .Where(e => typeof(ISoftDelete).IsAssignableFrom(e.ClrType)))
{
    var parameter = Expression.Parameter(entityType.ClrType, "e");
    var filter = Expression.Lambda(
        Expression.Equal(
            Expression.Property(parameter, nameof(ISoftDelete.IsDeleted)),
            Expression.Constant(false)),
        parameter);

    modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
}

// Soft delete method
public async Task SoftDeleteAsync<T>(T entity) where T : class, ISoftDelete
{
    entity.IsDeleted = true;
    entity.DeletedAt = DateTime.UtcNow;
    await _context.SaveChangesAsync();
}

Migrations

Commands

# Add migration
dotnet ef migrations add MigrationName

# Apply migrations
dotnet ef database update

# Generate SQL script
dotnet ef migrations script --idempotent -o migrate.sql

# Remove last migration
dotnet ef migrations remove

Data Migration

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql("UPDATE Products SET Price = Price * 1.1");
}

Common Interview Answers

Q: Eager vs Lazy vs Explicit loading?

  • Eager: Load with main query (Include)
  • Lazy: Load on property access (N+1 problem!)
  • Explicit: Load on demand with Entry().Load()

Q: What is AsSplitQuery?

  • Splits single query with multiple includes into separate queries
  • Avoids cartesian explosion with large result sets
  • Trade-off: Multiple roundtrips vs single large query

Q: How to handle concurrency?

  • Use RowVersion/Timestamp column
  • Catch DbUpdateConcurrencyException
  • Decide: Client wins, DB wins, or merge

Q: TPH vs TPT vs TPC?

  • TPH: Single table, discriminator column (fastest queries)
  • TPT: Separate tables, JOINs (normalized, slower)
  • TPC: Concrete tables only (no base table, .NET 7+)

Q: How to optimize read-heavy workloads?

  • AsNoTracking() for read-only queries
  • Projections (Select only needed columns)
  • Compiled queries for frequently called queries
  • Split queries for complex includes

Performance Tips

// 1. Use projections
.Select(p => new { p.Id, p.Name })

// 2. No tracking for reads
.AsNoTracking()

// 3. Batch operations
_context.BulkInsert(entities);  // Use EFCore.BulkExtensions

// 4. Index frequently queried columns
modelBuilder.Entity<Product>()
    .HasIndex(p => p.CategoryId);

// 5. Avoid N+1 with Include
.Include(o => o.Items)

// 6. Use FindAsync for single entity by PK
var product = await _context.Products.FindAsync(id);