Claude
Skill
dotnet-entity-framework-core
Design, tune, or review EF Core data access with proper modeling, migrations, query translation, performance, and lifetime management for modern .NET applications.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-entity-framework-core-bfa4ebd.zip · 9 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-entity-framework-core
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Entity Framework Core
Trigger On
- working on
DbContext, migrations, model configuration, or EF queries - reviewing tracking, loading, performance, or transaction behavior
- porting data access from EF6 or custom repositories to EF Core
- optimizing slow database queries
Documentation
References
- patterns.md - Query patterns, tracking strategies, loading strategies, projections, compiled queries, pagination, and temporal tables
- anti-patterns.md - Common EF Core mistakes including N+1 queries, large contexts, generic repositories, and missing indexes
Workflow
- Prefer EF Core for new development unless a documented gap requires Dapper or raw SQL
- Keep
DbContextlifetime scoped — align with unit of work - Review query translation — check generated SQL, avoid N+1
- Treat migrations as first-class — reviewable, not throwaway
- Be deliberate about provider behavior — cross-provider but not identical
- Validate with query inspection — not just in-memory mental model
DbContext Patterns
Basic Configuration
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
// Entity Configuration (Fluent API)
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
builder.HasIndex(p => p.Sku).IsUnique();
builder.HasMany(p => p.OrderItems).WithOne(oi => oi.Product);
}
}
Registration with DI
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.EnableSensitiveDataLogging() // Dev only
.EnableDetailedErrors()); // Dev only
// Or with pooling (better performance)
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString));
Query Patterns
Use AsNoTracking for Read-Only
// Bad - tracks entities unnecessarily
var products = await db.Products.ToListAsync();
// Good - no tracking overhead
var products = await db.Products
.AsNoTracking()
.ToListAsync();
Project to DTOs
// Bad - loads entire entity graph
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.ToListAsync();
// Good - loads only needed data
var orders = await db.Orders
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer.Name,
ItemCount = o.Items.Count,
Total = o.Items.Sum(i => i.Price)
})
.ToListAsync();
Avoid N+1 Queries
// Bad - N+1 problem
foreach (var order in orders)
{
var items = await db.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync();
}
// Good - eager loading
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
// Good - split query for large graphs
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
Compiled Queries (EF Core 9)
// Pre-compiled for frequently used queries
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
// Usage
var product = await GetProductById(db, productId);
Migration Patterns
Creating Migrations
# Add migration
dotnet ef migrations add AddProductIndex
# Apply to database
dotnet ef database update
# Generate SQL script
dotnet ef migrations script --idempotent -o migrate.sql
Data Migrations
public partial class AddProductIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Products_Sku",
table: "Products",
column: "Sku",
unique: true);
// Data migration (if needed)
migrationBuilder.Sql(@"
UPDATE Products
SET NormalizedName = UPPER(Name)
WHERE NormalizedName IS NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Products_Sku",
table: "Products");
}
}
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
ToList() then filter |
Loads all data to memory | Filter in query |
| Multiple DbContext per request | Transaction issues | Scoped lifetime |
| Lazy loading everywhere | N+1 queries | Explicit Include |
| Generic repository wrapper | Removes query power | Use DbContext directly |
| Ignoring generated SQL | Hidden performance issues | Log and review |
SaveChanges() in loops |
Many roundtrips | Batch then save |
Performance Best Practices
Index frequently queried columns:
builder.HasIndex(p => p.CreatedAt); builder.HasIndex(p => new { p.Category, p.Status });Use pagination:
var page = await db.Products .OrderBy(p => p.Id) .Skip(pageSize * pageNumber) .Take(pageSize) .ToListAsync();Batch updates (EF Core 7+):
await db.Products .Where(p => p.Category == "Obsolete") .ExecuteDeleteAsync(); await db.Products .Where(p => p.Category == "Sale") .ExecuteUpdateAsync(p => p.SetProperty(x => x.Price, x => x.Price * 0.9m));Minimize network roundtrips:
// Bad - 3 roundtrips var product = await db.Products.FindAsync(id); var reviews = await db.Reviews.Where(r => r.ProductId == id).ToListAsync(); var related = await db.Products.Where(p => p.Category == product.Category).ToListAsync(); // Good - 1 roundtrip var data = await db.Products .Where(p => p.Id == id) .Select(p => new { Product = p, Reviews = p.Reviews, Related = db.Products.Where(r => r.Category == p.Category).Take(5) }) .FirstOrDefaultAsync();
Concurrency Patterns
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[ConcurrencyCheck]
public int Version { get; set; }
// Or use RowVersion
[Timestamp]
public byte[] RowVersion { get; set; }
}
// Handle concurrency conflicts
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// Resolve conflict...
}
Deliver
- EF Core models and queries that match the domain
- safer migrations and lifetime management
- performance-aware data access decisions
- proper indexing and query optimization
Validate
- query behavior is intentional (check SQL logs)
- migrations are reviewable and correct
- no N+1 queries in common paths
- indexes exist for filtered/sorted columns
- DbContext lifetime is scoped properly
- concurrency is handled for critical entities
Files (dotnet-skills)
-
references
-
anti-patterns.md 12.5 KB
# EF Core Anti-Patterns ## N+1 Query Problem ### The Problem Loading related data in a loop causes one query per iteration: ```csharp // BAD: N+1 queries - 1 for orders + N for items var orders = await db.Orders.ToListAsync(); foreach (var order in orders) { // Each iteration executes a new query order.Items = await db.OrderItems .Where(i => i.OrderId == order.Id) .ToListAsync(); } ``` ### The Solution Use eager loading, projection, or explicit loading: ```csharp // GOOD: Single query with Include var orders = await db.Orders .Include(o => o.Items) .ToListAsync(); // GOOD: Split query for large graphs var orders = await db.Orders .Include(o => o.Items) .Include(o => o.Payments) .AsSplitQuery() .ToListAsync(); // GOOD: Projection to DTO var orderDtos = await db.Orders .Select(o => new OrderDto { Id = o.Id, Items = o.Items.Select(i => new OrderItemDto { ... }).ToList() }) .ToListAsync(); ``` ## Large DbContext with Too Many DbSets ### The Problem A single DbContext with dozens of DbSets becomes hard to maintain and test: ```csharp // BAD: Monolithic context public class AppDbContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<Product> Products { get; set; } public DbSet<Inventory> Inventory { get; set; } public DbSet<Shipment> Shipments { get; set; } public DbSet<Invoice> Invoices { get; set; } // ... 50 more DbSets } ``` ### The Solution Split into bounded contexts: ```csharp // GOOD: Bounded contexts public class OrderingDbContext : DbContext { public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { get; set; } } public class InventoryDbContext : DbContext { public DbSet<Product> Products { get; set; } public DbSet<StockLevel> StockLevels { get; set; } } ``` ## Loading Full Entities When You Need Subsets ### The Problem Fetching entire entity graphs when only a few properties are needed: ```csharp // BAD: Loads everything including large blobs var products = await db.Products .Include(p => p.Images) .Include(p => p.Reviews) .Include(p => p.Specifications) .ToListAsync(); // Then only using name and price var displayList = products.Select(p => $"{p.Name}: {p.Price}"); ``` ### The Solution Project to DTOs or anonymous types: ```csharp // GOOD: Only fetch what you need var products = await db.Products .Select(p => new { p.Name, p.Price }) .ToListAsync(); var displayList = products.Select(p => $"{p.Name}: {p.Price}"); ``` ## Client-Side Evaluation Without Awareness ### The Problem Filtering happens in memory instead of database: ```csharp // BAD: Custom method forces client evaluation var activeProducts = await db.Products .Where(p => IsProductActive(p)) // Cannot translate to SQL .ToListAsync(); // BAD: Complex string operations may not translate var products = await db.Products .Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) .ToListAsync(); ``` ### The Solution Use translatable expressions or explicit client evaluation: ```csharp // GOOD: Use EF.Functions for database operations var products = await db.Products .Where(p => EF.Functions.Like(p.Name, $"%{searchTerm}%")) .ToListAsync(); // GOOD: Explicit about client evaluation var allProducts = await db.Products.ToListAsync(); var activeProducts = allProducts.Where(p => IsProductActive(p)); ``` ## SaveChanges in Loops ### The Problem Calling SaveChanges repeatedly causes many database roundtrips: ```csharp // BAD: N roundtrips foreach (var product in products) { product.Price *= 1.1m; await db.SaveChangesAsync(); // Roundtrip each iteration } ``` ### The Solution Batch changes and save once: ```csharp // GOOD: Single roundtrip foreach (var product in products) { product.Price *= 1.1m; } await db.SaveChangesAsync(); // BETTER: Use ExecuteUpdate for bulk operations (EF Core 7+) await db.Products .Where(p => p.Category == category) .ExecuteUpdateAsync(s => s.SetProperty(p => p.Price, p => p.Price * 1.1m)); ``` ## Incorrect DbContext Lifetime ### The Problem Long-lived or singleton DbContext causes memory leaks and stale data: ```csharp // BAD: Singleton - accumulates tracked entities services.AddSingleton<AppDbContext>(); // BAD: Static or field-level context public class ProductService { private static readonly AppDbContext _db = new AppDbContext(); } ``` ### The Solution Use scoped lifetime aligned with unit of work: ```csharp // GOOD: Scoped lifetime (default for AddDbContext) services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString)); // GOOD: Pooled for better performance services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(connectionString)); // For background services, create scope explicitly public class BackgroundProcessor : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; protected override async Task ExecuteAsync(CancellationToken ct) { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // Use db within this scope } } ``` ## Generic Repository Anti-Pattern ### The Problem Wrapping DbContext in a generic repository hides EF Core's power: ```csharp // BAD: Generic repository loses query composition public interface IRepository<T> { Task<T> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); } // Usage - can't compose queries efficiently var products = await _productRepo.GetAllAsync(); var filtered = products.Where(p => p.Price > 100); // Client-side! ``` ### The Solution Use DbContext directly or create specific query methods: ```csharp // GOOD: Direct DbContext usage var products = await db.Products .Where(p => p.Price > 100) .OrderBy(p => p.Name) .Take(20) .ToListAsync(); // GOOD: Specific repository with meaningful methods public class ProductRepository { private readonly AppDbContext _db; public async Task<List<Product>> GetExpensiveProductsAsync(decimal minPrice) { return await _db.Products .Where(p => p.Price >= minPrice) .OrderByDescending(p => p.Price) .ToListAsync(); } } ``` ## Ignoring Query Translation ### The Problem Not verifying that LINQ translates to efficient SQL: ```csharp // May not translate as expected var results = await db.Products .Where(p => SomeComplexMethod(p)) .ToListAsync(); // Could load ALL products to memory! ``` ### The Solution Enable logging and verify SQL: ```csharp // In development services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString) .EnableSensitiveDataLogging() .EnableDetailedErrors() .LogTo(Console.WriteLine, LogLevel.Information)); // In tests - use ToQueryString() var query = db.Products.Where(p => p.Price > 100); var sql = query.ToQueryString(); Console.WriteLine(sql); ``` ## Lazy Loading Without Understanding ### The Problem Enabling lazy loading without realizing the N+1 implications: ```csharp // Configuration enables lazy loading services.AddDbContext<AppDbContext>(o => o.UseLazyLoadingProxies()); // BAD: Hidden N+1 queries in views/serialization @foreach (var order in orders) { <p>@order.Customer.Name</p> // Query per iteration! @foreach (var item in order.Items) // Another query per order! { <p>@item.Product.Name</p> // Yet another query! } } ``` ### The Solution Disable lazy loading and use explicit strategies: ```csharp // GOOD: Explicit eager loading for known needs var orders = await db.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(i => i.Product) .ToListAsync(); // Or project to view model var orderViews = await db.Orders .Select(o => new OrderViewModel { CustomerName = o.Customer.Name, Items = o.Items.Select(i => new ItemViewModel { ProductName = i.Product.Name }).ToList() }) .ToListAsync(); ``` ## Missing Indexes ### The Problem Queries filter on columns without indexes: ```csharp // If no index on Email, this scans entire table var user = await db.Users .FirstOrDefaultAsync(u => u.Email == email); // Composite filter without composite index var orders = await db.Orders .Where(o => o.CustomerId == customerId && o.Status == status) .ToListAsync(); ``` ### The Solution Add indexes for queried columns: ```csharp public class UserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.HasIndex(u => u.Email).IsUnique(); } } public class OrderConfiguration : IEntityTypeConfiguration<Order> { public void Configure(EntityTypeBuilder<Order> builder) { // Composite index matches query pattern builder.HasIndex(o => new { o.CustomerId, o.Status }); // Filtered index for common queries builder.HasIndex(o => o.CreatedAt) .HasFilter("[Status] = 'Pending'"); } } ``` ## Unbounded Queries ### The Problem Queries that can return unlimited results: ```csharp // BAD: Could return millions of rows var products = await db.Products.ToListAsync(); // BAD: User-controlled search without limits var results = await db.Products .Where(p => p.Name.Contains(searchTerm)) .ToListAsync(); ``` ### The Solution Always apply limits and pagination: ```csharp // GOOD: Bounded results var products = await db.Products .Take(100) .ToListAsync(); // GOOD: Paginated var results = await db.Products .Where(p => p.Name.Contains(searchTerm)) .OrderBy(p => p.Name) .Skip(page * pageSize) .Take(pageSize) .ToListAsync(); ``` ## Mixing Tracked and Untracked Entities ### The Problem Attaching or mixing entities from different tracking contexts: ```csharp // BAD: Entity from one context used in another var product = await db1.Products.FindAsync(id); db2.Products.Update(product); // Confusion and potential errors await db2.SaveChangesAsync(); // BAD: Mixing tracked and untracked var product = await db.Products.AsNoTracking().FirstAsync(p => p.Id == id); product.Price = newPrice; await db.SaveChangesAsync(); // Nothing saved - not tracked! ``` ### The Solution Be consistent with tracking and context usage: ```csharp // GOOD: Clear ownership var product = await db.Products.FindAsync(id); product.Price = newPrice; await db.SaveChangesAsync(); // GOOD: Explicit attach for disconnected scenarios var product = GetProductFromDto(dto); // Untracked db.Products.Update(product); // Marks all as modified await db.SaveChangesAsync(); // BETTER: Only update changed properties var product = await db.Products.FindAsync(dto.Id); product.Price = dto.Price; // Only this is marked modified await db.SaveChangesAsync(); ``` ## Not Using Transactions for Multi-Step Operations ### The Problem Multiple SaveChanges without transaction can leave data inconsistent: ```csharp // BAD: Partial failure possible order.Status = OrderStatus.Completed; await db.SaveChangesAsync(); inventory.Quantity -= order.Quantity; await db.SaveChangesAsync(); // If this fails, order status is wrong payment.Status = PaymentStatus.Captured; await db.SaveChangesAsync(); ``` ### The Solution Use explicit transactions: ```csharp // GOOD: All-or-nothing using var transaction = await db.Database.BeginTransactionAsync(); try { order.Status = OrderStatus.Completed; await db.SaveChangesAsync(); inventory.Quantity -= order.Quantity; await db.SaveChangesAsync(); payment.Status = PaymentStatus.Captured; await db.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } // BETTER: Single SaveChanges when possible order.Status = OrderStatus.Completed; inventory.Quantity -= order.Quantity; payment.Status = PaymentStatus.Captured; await db.SaveChangesAsync(); // All in one transaction ``` ## String-Based Includes ### The Problem Using string-based includes loses compile-time safety: ```csharp // BAD: Typos not caught at compile time var orders = await db.Orders .Include("Cusotmer") // Typo - runtime error .Include("Items.Prodcut") // Another typo .ToListAsync(); ``` ### The Solution Use strongly-typed lambda expressions: ```csharp // GOOD: Compile-time safety var orders = await db.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(i => i.Product) .ToListAsync(); ``` -
patterns.md 8.9 KB
# EF Core Query Patterns ## Query Tracking Strategies ### No-Tracking Queries Use `AsNoTracking()` for read-only scenarios to reduce memory overhead and improve performance: ```csharp // Single query var products = await db.Products .AsNoTracking() .Where(p => p.IsActive) .ToListAsync(); // Context-wide default (useful for read-heavy contexts) db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ``` ### No-Tracking with Identity Resolution When you need consistent object references without change tracking: ```csharp var orders = await db.Orders .AsNoTrackingWithIdentityResolution() .Include(o => o.Customer) .Include(o => o.Items) .ToListAsync(); // Same customer object returned for orders with same customer ``` ### Tracking Queries Use tracking only when you intend to modify entities: ```csharp var product = await db.Products.FindAsync(id); product.Price = newPrice; await db.SaveChangesAsync(); ``` ## Loading Strategies ### Eager Loading Load related data in a single query using `Include()`: ```csharp // Single level var orders = await db.Orders .Include(o => o.Customer) .ToListAsync(); // Nested includes var orders = await db.Orders .Include(o => o.Items) .ThenInclude(i => i.Product) .ThenInclude(p => p.Category) .ToListAsync(); // Multiple includes var orders = await db.Orders .Include(o => o.Customer) .Include(o => o.ShippingAddress) .Include(o => o.Items) .ToListAsync(); ``` ### Filtered Includes (EF Core 5+) Include only specific related entities: ```csharp var orders = await db.Orders .Include(o => o.Items.Where(i => i.Quantity > 0)) .ToListAsync(); // With ordering and limiting var customers = await db.Customers .Include(c => c.Orders .OrderByDescending(o => o.CreatedAt) .Take(5)) .ToListAsync(); ``` ### Split Queries Avoid cartesian explosion with large entity graphs: ```csharp // Without split - one large query with cartesian product var orders = await db.Orders .Include(o => o.Items) .Include(o => o.Payments) .ToListAsync(); // With split - multiple smaller queries var orders = await db.Orders .Include(o => o.Items) .Include(o => o.Payments) .AsSplitQuery() .ToListAsync(); // Configure as default optionsBuilder.UseSqlServer(connectionString, o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); ``` ### Explicit Loading Load related data on demand after the principal entity: ```csharp var order = await db.Orders.FindAsync(orderId); // Load collection await db.Entry(order) .Collection(o => o.Items) .LoadAsync(); // Load reference await db.Entry(order) .Reference(o => o.Customer) .LoadAsync(); // Query into loaded navigation var highValueItems = await db.Entry(order) .Collection(o => o.Items) .Query() .Where(i => i.Price > 100) .ToListAsync(); ``` ### Lazy Loading Requires proxy setup and virtual navigation properties: ```csharp // Setup services.AddDbContext<AppDbContext>(options => options.UseLazyLoadingProxies() .UseSqlServer(connectionString)); // Entity public class Order { public int Id { get; set; } public virtual Customer Customer { get; set; } // virtual required public virtual ICollection<OrderItem> Items { get; set; } } ``` **Warning:** Lazy loading often leads to N+1 queries. Prefer explicit strategies. ## Projection Patterns ### Project to DTOs Always project when you need a subset of data: ```csharp var orderSummaries = await db.Orders .Where(o => o.Status == OrderStatus.Pending) .Select(o => new OrderSummaryDto { OrderId = o.Id, CustomerName = o.Customer.Name, ItemCount = o.Items.Count, Total = o.Items.Sum(i => i.Quantity * i.UnitPrice), CreatedAt = o.CreatedAt }) .ToListAsync(); ``` ### Anonymous Projections For internal use without creating DTOs: ```csharp var stats = await db.Products .GroupBy(p => p.Category) .Select(g => new { Category = g.Key, Count = g.Count(), AvgPrice = g.Average(p => p.Price), MaxPrice = g.Max(p => p.Price) }) .ToListAsync(); ``` ### Conditional Projection ```csharp var products = await db.Products .Select(p => new ProductDto { Id = p.Id, Name = p.Name, DisplayPrice = p.IsOnSale ? p.SalePrice : p.RegularPrice, StockStatus = p.Stock > 10 ? "In Stock" : p.Stock > 0 ? "Low Stock" : "Out of Stock" }) .ToListAsync(); ``` ## Compiled Queries Pre-compile frequently used queries for better performance: ```csharp public static class CompiledQueries { public static readonly Func<AppDbContext, int, Task<Product?>> GetProductById = EF.CompileAsyncQuery((AppDbContext db, int id) => db.Products.FirstOrDefault(p => p.Id == id)); public static readonly Func<AppDbContext, string, IAsyncEnumerable<Product>> GetProductsByCategory = EF.CompileAsyncQuery((AppDbContext db, string category) => db.Products.Where(p => p.Category == category)); public static readonly Func<AppDbContext, decimal, int, IAsyncEnumerable<Product>> GetExpensiveProducts = EF.CompileAsyncQuery((AppDbContext db, decimal minPrice, int take) => db.Products .Where(p => p.Price >= minPrice) .OrderByDescending(p => p.Price) .Take(take)); } // Usage var product = await CompiledQueries.GetProductById(db, productId); await foreach (var p in CompiledQueries.GetProductsByCategory(db, "Electronics")) { // Process product } ``` ## Raw SQL Patterns ### FromSql for Entity Queries ```csharp var products = await db.Products .FromSql($"SELECT * FROM Products WHERE Price > {minPrice}") .ToListAsync(); // Composable - can add LINQ operators var products = await db.Products .FromSql($"SELECT * FROM Products WHERE Category = {category}") .Where(p => p.IsActive) .OrderBy(p => p.Name) .ToListAsync(); ``` ### SqlQuery for Arbitrary Results (EF Core 8+) ```csharp var totals = await db.Database .SqlQuery<decimal>($"SELECT SUM(Price) FROM Products WHERE Category = {category}") .ToListAsync(); var stats = await db.Database .SqlQuery<CategoryStats>($@" SELECT Category, COUNT(*) as ProductCount, AVG(Price) as AvgPrice FROM Products GROUP BY Category") .ToListAsync(); ``` ### ExecuteSql for Non-Query Operations ```csharp var affected = await db.Database .ExecuteSqlAsync($"UPDATE Products SET Price = Price * {multiplier} WHERE Category = {category}"); ``` ## Pagination Patterns ### Offset-Based Pagination ```csharp public async Task<PagedResult<T>> GetPageAsync<T>( IQueryable<T> query, int pageNumber, int pageSize) { var totalCount = await query.CountAsync(); var items = await query .Skip(pageNumber * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResult<T> { Items = items, TotalCount = totalCount, PageNumber = pageNumber, PageSize = pageSize, TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize) }; } ``` ### Keyset Pagination (Better for Large Datasets) ```csharp // More efficient for deep pages - uses index instead of offset public async Task<List<Product>> GetNextPageAsync(int lastId, int pageSize) { return await db.Products .Where(p => p.Id > lastId) .OrderBy(p => p.Id) .Take(pageSize) .ToListAsync(); } // Bidirectional keyset pagination public async Task<List<Product>> GetPreviousPageAsync(int firstId, int pageSize) { return await db.Products .Where(p => p.Id < firstId) .OrderByDescending(p => p.Id) .Take(pageSize) .OrderBy(p => p.Id) // Restore ascending order .ToListAsync(); } ``` ## Global Query Filters Apply filters automatically to all queries: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { // Soft delete filter modelBuilder.Entity<Product>() .HasQueryFilter(p => !p.IsDeleted); // Multi-tenant filter modelBuilder.Entity<Order>() .HasQueryFilter(o => o.TenantId == _tenantId); } // Bypass when needed var allProducts = await db.Products .IgnoreQueryFilters() .ToListAsync(); ``` ## Temporal Tables (EF Core 6+) Query historical data: ```csharp // Configure temporal table modelBuilder.Entity<Product>() .ToTable("Products", b => b.IsTemporal()); // Query as of specific time var historicalProducts = await db.Products .TemporalAsOf(specificDateTime) .ToListAsync(); // Query between time range var productHistory = await db.Products .TemporalBetween(startDate, endDate) .Where(p => p.Id == productId) .ToListAsync(); // Get all changes var allChanges = await db.Products .TemporalAll() .Where(p => p.Id == productId) .OrderBy(p => EF.Property<DateTime>(p, "PeriodStart")) .ToListAsync(); ```
-
-
SKILL.md 8.1 KB
--- name: dotnet-entity-framework-core version: "1.0.0" category: "Data" description: "Design, tune, or review EF Core data access with proper modeling, migrations, query translation, performance, and lifetime management for modern .NET applications." compatibility: "Requires EF Core 7+ (preferably 8/9 for latest features)." --- # Entity Framework Core ## Trigger On - working on `DbContext`, migrations, model configuration, or EF queries - reviewing tracking, loading, performance, or transaction behavior - porting data access from EF6 or custom repositories to EF Core - optimizing slow database queries ## Documentation - [EF Core Overview](https://learn.microsoft.com/en-us/ef/core/) - [Performance](https://learn.microsoft.com/en-us/ef/core/performance/) - [Efficient Querying](https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying) - [Migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) - [What's New in EF Core 9](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew) ### References - [patterns.md](references/patterns.md) - Query patterns, tracking strategies, loading strategies, projections, compiled queries, pagination, and temporal tables - [anti-patterns.md](references/anti-patterns.md) - Common EF Core mistakes including N+1 queries, large contexts, generic repositories, and missing indexes ## Workflow 1. **Prefer EF Core for new development** unless a documented gap requires Dapper or raw SQL 2. **Keep `DbContext` lifetime scoped** — align with unit of work 3. **Review query translation** — check generated SQL, avoid N+1 4. **Treat migrations as first-class** — reviewable, not throwaway 5. **Be deliberate about provider behavior** — cross-provider but not identical 6. **Validate with query inspection** — not just in-memory mental model ## DbContext Patterns ### Basic Configuration ```csharp public class AppDbContext : DbContext { public DbSet<Product> Products => Set<Product>(); public DbSet<Order> Orders => Set<Order>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); } } // Entity Configuration (Fluent API) public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.HasKey(p => p.Id); builder.Property(p => p.Name).HasMaxLength(200).IsRequired(); builder.HasIndex(p => p.Sku).IsUnique(); builder.HasMany(p => p.OrderItems).WithOne(oi => oi.Product); } } ``` ### Registration with DI ```csharp builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString) .EnableSensitiveDataLogging() // Dev only .EnableDetailedErrors()); // Dev only // Or with pooling (better performance) builder.Services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(connectionString)); ``` ## Query Patterns ### Use AsNoTracking for Read-Only ```csharp // Bad - tracks entities unnecessarily var products = await db.Products.ToListAsync(); // Good - no tracking overhead var products = await db.Products .AsNoTracking() .ToListAsync(); ``` ### Project to DTOs ```csharp // Bad - loads entire entity graph var orders = await db.Orders .Include(o => o.Items) .Include(o => o.Customer) .ToListAsync(); // Good - loads only needed data var orders = await db.Orders .Select(o => new OrderDto { Id = o.Id, CustomerName = o.Customer.Name, ItemCount = o.Items.Count, Total = o.Items.Sum(i => i.Price) }) .ToListAsync(); ``` ### Avoid N+1 Queries ```csharp // Bad - N+1 problem foreach (var order in orders) { var items = await db.OrderItems .Where(i => i.OrderId == order.Id) .ToListAsync(); } // Good - eager loading var orders = await db.Orders .Include(o => o.Items) .ToListAsync(); // Good - split query for large graphs var orders = await db.Orders .Include(o => o.Items) .AsSplitQuery() .ToListAsync(); ``` ### Compiled Queries (EF Core 9) ```csharp // Pre-compiled for frequently used queries private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById = EF.CompileAsyncQuery((AppDbContext db, int id) => db.Products.FirstOrDefault(p => p.Id == id)); // Usage var product = await GetProductById(db, productId); ``` ## Migration Patterns ### Creating Migrations ```bash # Add migration dotnet ef migrations add AddProductIndex # Apply to database dotnet ef database update # Generate SQL script dotnet ef migrations script --idempotent -o migrate.sql ``` ### Data Migrations ```csharp public partial class AddProductIndex : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateIndex( name: "IX_Products_Sku", table: "Products", column: "Sku", unique: true); // Data migration (if needed) migrationBuilder.Sql(@" UPDATE Products SET NormalizedName = UPPER(Name) WHERE NormalizedName IS NULL"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropIndex( name: "IX_Products_Sku", table: "Products"); } } ``` ## Anti-Patterns to Avoid | Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | `ToList()` then filter | Loads all data to memory | Filter in query | | Multiple DbContext per request | Transaction issues | Scoped lifetime | | Lazy loading everywhere | N+1 queries | Explicit Include | | Generic repository wrapper | Removes query power | Use DbContext directly | | Ignoring generated SQL | Hidden performance issues | Log and review | | `SaveChanges()` in loops | Many roundtrips | Batch then save | ## Performance Best Practices 1. **Index frequently queried columns:** ```csharp builder.HasIndex(p => p.CreatedAt); builder.HasIndex(p => new { p.Category, p.Status }); ``` 2. **Use pagination:** ```csharp var page = await db.Products .OrderBy(p => p.Id) .Skip(pageSize * pageNumber) .Take(pageSize) .ToListAsync(); ``` 3. **Batch updates (EF Core 7+):** ```csharp await db.Products .Where(p => p.Category == "Obsolete") .ExecuteDeleteAsync(); await db.Products .Where(p => p.Category == "Sale") .ExecuteUpdateAsync(p => p.SetProperty(x => x.Price, x => x.Price * 0.9m)); ``` 4. **Minimize network roundtrips:** ```csharp // Bad - 3 roundtrips var product = await db.Products.FindAsync(id); var reviews = await db.Reviews.Where(r => r.ProductId == id).ToListAsync(); var related = await db.Products.Where(p => p.Category == product.Category).ToListAsync(); // Good - 1 roundtrip var data = await db.Products .Where(p => p.Id == id) .Select(p => new { Product = p, Reviews = p.Reviews, Related = db.Products.Where(r => r.Category == p.Category).Take(5) }) .FirstOrDefaultAsync(); ``` ## Concurrency Patterns ```csharp public class Product { public int Id { get; set; } public string Name { get; set; } [ConcurrencyCheck] public int Version { get; set; } // Or use RowVersion [Timestamp] public byte[] RowVersion { get; set; } } // Handle concurrency conflicts try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = await entry.GetDatabaseValuesAsync(); // Resolve conflict... } ``` ## Deliver - EF Core models and queries that match the domain - safer migrations and lifetime management - performance-aware data access decisions - proper indexing and query optimization ## Validate - query behavior is intentional (check SQL logs) - migrations are reviewable and correct - no N+1 queries in common paths - indexes exist for filtered/sorted columns - DbContext lifetime is scoped properly - concurrency is handled for critical entities
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.