Entity Framework
Entity Framework Core ORM for data access, migrations, and query optimization in .NET
You are an expert in Entity Framework Core for building data access layers in .NET applications. ## Key Points - **Always use `AsNoTracking()`** for read-only queries to avoid the overhead of change tracking. - **Project to DTOs with `Select()`** instead of loading full entity graphs with `Include()`. - **Use `IEntityTypeConfiguration`** in separate files rather than crowding `OnModelCreating`. - **Generate idempotent SQL scripts** for production deployments instead of running `dotnet ef database update` directly. - **Register `DbContext` with `AddDbContextPool`** for high-throughput APIs to reuse context instances. - **Pass `CancellationToken`** through to all async EF Core calls so requests can be cancelled gracefully. - **N+1 queries** — accessing a navigation property in a loop without `Include()` or projection triggers a query per iteration. Use eager loading or `Select()` projections. - **Tracking queries for read-only data** — adds memory pressure and slows down queries. Add `AsNoTracking()` or configure it at the context level with `QueryTrackingBehavior.NoTracking`. - **Calling `ToList()` before filtering** — materializes the entire table into memory. Always apply `Where()` and pagination before `ToListAsync()`. - **Mixing `DbContext` across threads** — `DbContext` is not thread-safe. Use a scoped lifetime and never share a context between concurrent operations. - **Ignoring migration bundles** — relying on `EnsureCreated()` in production skips the migration history entirely and will break on schema changes.
skilldb get csharp-dotnet-skills/Entity FrameworkFull skill: 226 linesEntity Framework Core — C#/.NET
You are an expert in Entity Framework Core for building data access layers in .NET applications.
Core Philosophy
Overview
Entity Framework Core (EF Core) is the official ORM for .NET. It maps C# classes to database tables, translates LINQ queries to SQL, tracks entity changes, and manages schema evolution through migrations. EF Core supports SQL Server, PostgreSQL, SQLite, MySQL, and other databases through provider packages.
Core Concepts
DbContext and Entity Configuration
The DbContext is the unit-of-work that coordinates queries, change tracking, and persistence.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
Fluent Configuration with IEntityTypeConfiguration
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber)
.IsRequired()
.HasMaxLength(50);
builder.Property(o => o.Total)
.HasPrecision(18, 2);
builder.HasMany(o => o.Items)
.WithOne(i => i.Order)
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(o => o.OrderNumber).IsUnique();
}
}
Migrations
# Create a migration
dotnet ef migrations add AddOrdersTable
# Apply migrations
dotnet ef database update
# Generate SQL script for production
dotnet ef migrations script --idempotent -o migrate.sql
Implementation Patterns
Repository Pattern (Thin Wrapper)
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id, CancellationToken ct = default);
Task<List<T>> GetAllAsync(CancellationToken ct = default);
void Add(T entity);
void Remove(T entity);
}
public class Repository<T>(AppDbContext db) : IRepository<T> where T : class
{
public async Task<T?> GetByIdAsync(int id, CancellationToken ct = default)
=> await db.Set<T>().FindAsync([id], ct);
public async Task<List<T>> GetAllAsync(CancellationToken ct = default)
=> await db.Set<T>().ToListAsync(ct);
public void Add(T entity) => db.Set<T>().Add(entity);
public void Remove(T entity) => db.Set<T>().Remove(entity);
}
public class UnitOfWork(AppDbContext db) : IUnitOfWork
{
public Task<int> SaveChangesAsync(CancellationToken ct = default)
=> db.SaveChangesAsync(ct);
}
Querying with Projections
// Bad: loads entire entity graph
var orders = await db.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ToListAsync();
// Good: project to DTO, only selects needed columns
var orderDtos = await db.Orders
.Where(o => o.Status == OrderStatus.Active)
.Select(o => new OrderDto
{
Id = o.Id,
OrderNumber = o.OrderNumber,
Total = o.Total,
ItemCount = o.Items.Count,
ProductNames = o.Items.Select(i => i.Product.Name).ToList()
})
.ToListAsync();
Compiled Queries for Hot Paths
public static class ProductQueries
{
public static readonly Func<AppDbContext, decimal, IAsyncEnumerable<Product>>
GetExpensiveProducts = EF.CompileAsyncQuery(
(AppDbContext db, decimal minPrice) =>
db.Products.Where(p => p.Price >= minPrice));
}
// Usage
await foreach (var product in ProductQueries.GetExpensiveProducts(db, 100m))
{
Console.WriteLine(product.Name);
}
Interceptors for Cross-Cutting Concerns
public class AuditInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context!;
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
if (entry.State == EntityState.Added)
entry.Entity.CreatedAt = DateTime.UtcNow;
if (entry.State == EntityState.Modified)
entry.Entity.UpdatedAt = DateTime.UtcNow;
}
return base.SavingChangesAsync(eventData, result, ct);
}
}
// Registration
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.AddInterceptors(new AuditInterceptor()));
Pagination
public record PagedResult<T>(List<T> Items, int TotalCount, int Page, int PageSize);
public static async Task<PagedResult<T>> ToPagedAsync<T>(
this IQueryable<T> query, int page, int pageSize, CancellationToken ct = default)
{
var totalCount = await query.CountAsync(ct);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(ct);
return new PagedResult<T>(items, totalCount, page, pageSize);
}
Best Practices
- Always use
AsNoTracking()for read-only queries to avoid the overhead of change tracking. - Project to DTOs with
Select()instead of loading full entity graphs withInclude(). - Use
IEntityTypeConfigurationin separate files rather than crowdingOnModelCreating. - Generate idempotent SQL scripts for production deployments instead of running
dotnet ef database updatedirectly. - Register
DbContextwithAddDbContextPoolfor high-throughput APIs to reuse context instances. - Pass
CancellationTokenthrough to all async EF Core calls so requests can be cancelled gracefully.
Common Pitfalls
- N+1 queries — accessing a navigation property in a loop without
Include()or projection triggers a query per iteration. Use eager loading orSelect()projections. - Tracking queries for read-only data — adds memory pressure and slows down queries. Add
AsNoTracking()or configure it at the context level withQueryTrackingBehavior.NoTracking. - Calling
ToList()before filtering — materializes the entire table into memory. Always applyWhere()and pagination beforeToListAsync(). - Mixing
DbContextacross threads —DbContextis not thread-safe. Use a scoped lifetime and never share a context between concurrent operations. - Ignoring migration bundles — relying on
EnsureCreated()in production skips the migration history entirely and will break on schema changes.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add csharp-dotnet-skills
Related Skills
Aspnet Minimal API
ASP.NET Minimal APIs for building lightweight HTTP endpoints in .NET
Async Patterns
Async/await patterns, Task-based concurrency, and cancellation in C#/.NET
Blazor
Blazor component model, rendering modes, and interactive web UI development in .NET
Configuration
.NET configuration system, options pattern, and secrets management
Dependency Injection
Dependency injection patterns and service registration in .NET's built-in DI container
Mediatr
MediatR library for implementing CQRS, commands, queries, and pipeline behaviors in .NET