关于 entity-framework-core
entity-framework-core 是用于构建和审查 EF Core 数据访问代码的开发技能,涵盖 DbContext、查询、迁移及性能决策,并提供数据库应用开发中的模式与反模式。
主要功能
- 01DbContext 生命周期与配置指导
- 02查询跟踪、加载和投影模式
- 03迁移与架构变更实践
- 04性能、索引和并发指导
- 05检查生成的 SQL 与 N+1 查询
适用场景
- 01设计 EF Core 模型和查询
- 02审查迁移与 DbContext 用法
- 03排查慢查询和 N+1 行为
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill entity-framework-core安装后在 Claude Code 中输入/entity-framework-core确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
manifest.jsonreferences/anti-patterns.mdpatterns.md详细介绍
Entity Framework Core
适用场景
- 使用
DbContext、迁移、模型配置或 EF 查询 - 审查跟踪、加载、性能或事务行为
- 将数据访问从 EF6 或自定义存储库迁移到 EF Core
- 优化缓慢的数据库查询
文档
参考资料
- patterns.md - 查询模式、跟踪策略、加载策略、投影、编译查询、分页和时态表
- anti-patterns.md - 常见 EF Core 错误,包括 N+1 查询、大型上下文、通用存储库和缺少索引
工作流
- 新开发优先使用 EF Core,除非有记录在案的缺口需要使用 Dapper 或原始 SQL
- 将
DbContext的生命周期限定为作用域——与工作单元保持一致 - 检查查询转换——检查生成的 SQL,避免 N+1 查询
- 将迁移视为一等事项——应可供审查,而非用完即弃
- 审慎处理提供程序行为——跨提供程序可用,但行为并不完全相同
- 通过检查查询进行验证——不要只依赖脑中的内存模型
上游最新说明
- EF Core
v10.0.11是一个维护版本。它修复了 Azure SQL 兼容级别 170 的 JSON 转换问题,使嵌套的OPENJSON投影保留AS JSON,并确保 EF 与 .NET 11 新增的MemoryExtensions.Min/Max重载保持兼容。升级后,请重新运行基于提供程序的 JSON 查询,并针对仓库选定的目标框架进行编译。 - 2026 年 8 月的 EF Core 与 EF6 对比仍是迁移决策的首要参考。EF Core 是当前活跃的跨平台技术栈,但对于仅使用 EF6 且大量依赖 EDMX/ObjectContext 的代码,在完成特性清单并通过基于数据库的等效性测试之前,不应迁移。
DbContext 模式
基本配置
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);
}
}
使用 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));
查询模式
只读时使用 AsNoTracking
// Bad - tracks entities unnecessarily
var products = await db.Products.ToListAsync();
// Good - no tracking overhead
var products = await db.Products
.AsNoTracking()
.ToListAsync();
投影到 DTO
// 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();
避免 N+1 查询
// 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();
编译查询(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);
迁移模式
创建迁移
# 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
数据迁移
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");
}
}
应避免的反模式
| 反模式 | 问题所在 | 更好的做法 |
|---|---|---|
ToList() 后再筛选 | 将所有数据加载到内存 | 在查询中筛选 |
| 每个请求使用多个 DbContext | 事务问题 | 作用域生命周期 |
| 到处使用延迟加载 | N+1 查询 | 显式使用 Include |
| 通用存储库包装器 | 削弱查询能力 | 直接使用 DbContext |
| 忽略生成的 SQL | 隐藏的性能问题 | 记录并审查 |
在循环中调用 SaveChanges() | 多次往返 | 批量处理后再保存 |
性能最佳实践
-
为经常查询的列建立索引:
builder.HasIndex(p => p.CreatedAt); builder.HasIndex(p => new { p.Category, p.Status }); -
使用分页:
var page = await db.Products .OrderBy(p => p.Id) .Skip(pageSize * pageNumber) .Take(pageSize) .ToListAsync(); -
批量更新(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)); -
尽量减少网络往返:
// 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();
并发模式
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...
}
交付内容
- 与领域相匹配的 EF Core 模型和查询
- 更安全的迁移和生命周期管理
- 考虑性能的数据访问决策
- 正确的索引和查询优化
验证
- 查询行为符合预期(检查 SQL 日志)
- 迁移可供审查且正确
- 常见路径中没有 N+1 查询
- 筛选或排序列上存在索引
DbContext生命周期正确限定为作用域- 关键实体的并发得到妥善处理
评论
支持 Markdown