skills/ef-core/SKILL.md
Entity Framework Core 開發規範:DbContext Lifetime、查詢效能、Migration 管理與變更追蹤最佳實踐。
npx skillsauth add CloudyWing/ai-dotfiles ef-coreInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
當偵測到專案使用 Entity Framework Core(含 Microsoft.EntityFrameworkCore 相依套件)或使用者要求撰寫資料存取邏輯時,請自動套用以下規範。
Scoped(AddDbContext<T> 預設行為)。IDbContextFactory<T> 並在使用處建立短生命週期實例。// ✅ 正確:背景服務使用 IDbContextFactory
public class MyBackgroundService(IDbContextFactory<AppDbContext> contextFactory) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
await using AppDbContext db = await contextFactory.CreateDbContextAsync(stoppingToken)
.ConfigureAwait(false);
// ...
}
}
// ❌ 錯誤:直接注入 DbContext 至 Singleton
public class MyBackgroundService(AppDbContext db) : BackgroundService { }
Include() / ThenInclude() 預先載入,或改用投影(Select)一次取回所需欄位。SaveChanges() 或 SaveChangesAsync(),應批次處理後統一儲存。.AsNoTracking(),降低記憶體與效能開銷。UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)。// ✅ 正確:唯讀查詢
IReadOnlyList<OrderDto> orders = await db.Orders
.AsNoTracking()
.Where(o => o.Status == OrderStatus.Completed)
.Select(o => new OrderDto {
Id = o.Id,
Total = o.Total
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// ❌ 錯誤:唯讀場景未加 AsNoTracking,且回傳整個 Entity
List<Order> orders = await db.Orders
.Where(o => o.Status == OrderStatus.Completed)
.ToListAsync(cancellationToken);
Select() 投影至 DTO,避免載入整個 Entity。IsDeleted)、多租戶(TenantId)等橫切條件,優先在 OnModelCreating 中透過 .HasQueryFilter() 統一設定。.IgnoreQueryFilters(),並加上註解說明原因。AddOrderStatusColumn),不使用自動產生的時間戳名稱。HasData() 方式,不在 Migration 的 Up() 方法中直接寫入 SQL Insert。SaveChangesAsync() 應在業務操作單元的最外層呼叫一次,不在 Repository 方法內部呼叫(避免破壞 Unit of Work 模式)。AddRangeAsync() 並控制批次大小,避免單次追蹤過多 Entity。SaveChangesAsync() 呼叫本身即為一個交易,簡單場景不需額外包裝。SaveChangesAsync() 或需要搭配其他外部操作時,使用 IDbContextTransaction:await using IDbContextTransaction transaction = await db.Database
.BeginTransactionAsync(cancellationToken)
.ConfigureAwait(false);
try {
// 多個操作...
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
} catch {
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
throw;
}
[Timestamp] Attribute(SQL Server rowversion)或 [ConcurrencyToken](任意欄位)。DbUpdateConcurrencyException,必須明確處理(重試或提示使用者)。public class Order {
public int Id { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } = [];
}
// 並發衝突處理
try {
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
} catch (DbUpdateConcurrencyException ex) {
// 重新從資料庫取得最新值,決定合併策略
await ex.Entries.Single().ReloadAsync(cancellationToken).ConfigureAwait(false);
throw;
}
Microsoft.EntityFrameworkCore.Proxies 並呼叫 .UseLazyLoadingProxies(),Navigation Property 將在存取時觸發額外查詢,容易引發隱性 N+1。Include()、ThenInclude() 或投影。FromSqlInterpolated() 或 FromSqlRaw()。FromSqlRaw() 時,必須透過參數化查詢防止 SQL Injection,禁止字串串接。// ✅ 正確:參數化
IReadOnlyList<Product> products = await db.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE CategoryId = {categoryId}")
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// ❌ 錯誤:字串串接
IReadOnlyList<Product> products = await db.Products
.FromSqlRaw("SELECT * FROM Products WHERE CategoryId = " + categoryId)
.ToListAsync(cancellationToken);
tools
PowerShell 腳本撰寫規範:嚴格模式、錯誤處理、參數宣告、Verb-Noun 命名與 5.1 相容語法邊界。當撰寫或修改 `*.ps1` / `*.psm1` 腳本時自動套用。
tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.