Cornhsu.Labeling
EF Core 沒有內建多型關聯——「標籤 A 貼在實體 B 上」的 B 可能是筆記、待辦、行事曆,每個專案都得土法煉鋼一次。我把自己桌面應用的標籤功能抽象成公開套件:一行註冊,連結表自動生成,每條連結都有真外鍵,刪除永遠不留孤兒資料。
services.AddLabeling<AppDbContext>(r => { r.Labelable<Note>(n => n.Title); // int 主鍵 r.Labelable<Memo>(m => m.Title); // Guid 主鍵,同 App 可混用 }); await store.AttachAsync(note, "論文", "急件"); var hits = await store.FindByLabelsAsync(new[] { "論文", "急件" }, LabelMatch.All);
把「每個專案都要重寫一次」的東西,做成一個套件
標籤幾乎是每個資料型應用的標配需求,但 EF Core 沒有內建多型關聯:想讓同一顆標籤同時貼在筆記、待辦、行事曆事件上,常見做法是共用連結表配 discriminator 欄位——代價是失去真外鍵,孤兒資料只能靠應用程式紀律防守。
Cornhsu.Labeling 以抽象層+EF Core 實作雙套件發佈於 NuGet(Cornhsu.* 前綴獲 NuGet 官方保留),核心手法是 LabelLink<TEntity,TKey> 封閉泛型:每個註冊型別由 EF 自動生成一張帶真外鍵的連結表,cascade 刪除由資料庫執行。
1.0 定版前,先把自家 WPF 應用 QuillNest 的自製標籤系統整個換成此套件,在使用者正式資料上跑通才發佈;效能以 5 型別 × 1 萬筆基準實測,查詢皆在 20ms 內,數據公開在 README。
一個 1.0,該有的都先做完
| 發佈 | NuGet 正式版 v1.0.0(抽象層+EF Core 實作雙套件);Cornhsu.* 前綴獲 NuGet 官方保留 |
| 測試 | 78 條(72 核心+6 Analyzer),含真外鍵完整性、並發衝突、混合主鍵 |
| CI 矩陣 | SQLite / SQL Server / PostgreSQL × EF Core 8 / 9 / 10,每次 push 全跑 |
| 真實驗證 | 1.0 之前先把自家 WPF 應用(QuillNest)整個遷移到此套件,在使用者正式資料上跑通才定版 |
| 發佈管線 | GitHub Actions + OIDC Trusted Publishing(無長效金鑰):推 tag 即自動測試、打包、上架 |
| 效能 | 5 型別 × 1 萬筆基準測試,查詢皆 < 20ms;數據公開在 README |
六個值得一提的設計
封閉泛型自動建表
核心手法是 LabelLink<TEntity,TKey> 封閉泛型——每個註冊型別由 EF 自動生成一張帶真外鍵的連結表(每型別一表)。孤兒資料在資料庫層就不可能出現,而不是靠應用程式紀律。
泛型主鍵、單一型別參數 API
主鍵型別(int / Guid / long / string)在註冊時以反射從 ILabelable<TKey> 推斷,使用者永遠只寫 r.Labelable<Note>();跨型別查詢結果以 EntityIdAs<TKey>() 取回強型別。
內建 Roslyn Analyzer
實作了 ILabelable<TKey> 卻忘記註冊 → 編譯期警告(CHSU001),不用等執行期才炸;隨套件出貨,安裝即生效。
並發保護不綁資料庫
ConcurrencyStamp 由 store 輪換而非依賴 rowversion,三個資料庫行為完全一致;兩邊同時修改同一標籤時,後存檔者得到明確例外而非默默覆蓋。
效能先量再優化
寫了 benchmark harness 實測後,保留樸素的每型別一次查詢(5 型別 × 1 萬筆 ≈ 19ms,夠用),把力氣花在真正的瓶頸——清單畫面 N+1(批次 API 實測 6×、省 49 次 roundtrip)。
兩種掛載方式
標準 DI(AddLabeling)之外,為無 DI 容器的 WPF / WinForms 應用提供 LabelStoreFactory——這是真實遷移時發現的缺口,而不是想像出來的需求。
規畫書 → 里程碑 → 真實產品驗收 → 才定版
抽象與實作
先寫規畫書拍板設計取捨(每型別一表 vs discriminator vs TPH、名稱全域唯一 vs 路徑定址),測試用 SQLite in-memory 跑真外鍵,不用 EF InMemory——它不執行外鍵約束,測不到核心保證。
發佈管線
Trusted Publishing(OIDC 換一小時臨時金鑰),repo 裡沒有任何秘密。
真實驗證(不可跳過)
把自家應用 QuillNest 的自製標籤系統整個刪掉改用套件——包含使用者正式資料的搬移 migration(int→Guid 對照、先在副本演練、備援升級路徑)。遷移過程真實地暴露了兩個 API 缺口(無 DI 入口、批次讀),都以正門補上而非開後門。
Schema 決定全部關帳
效能基準、三資料庫 × 三 EF 版本矩陣、並發語意定案——schema 影響的決定全部在 1.0 前做完,因為之後就是 breaking change。
打磨
Analyzer、批次貼標、可選 ILogger。全部完成後才 tag v1.0.0,API 穩定承諾生效。
先讓自己的產品當第一個使用者
1.0 定版前,QuillNest(WPF 個人生產力應用)的標籤系統整個換成本套件驅動——標籤管理、行事曆分類都跑在 Cornhsu.Labeling 上,包含使用者正式資料的搬移。套件的業務欄位(顏色、圖示、類型)由消費端伴生表實作,正是「Label 保持精簡」取捨的實際示範。


取了什麼、捨了什麼、為什麼
| 決定 | 取 | 捨 | 理由 |
|---|---|---|---|
| 每型別一張連結表 | 真外鍵、cascade、可加索引 | 表數量隨型別成長 | 資料完整性是套件存在的理由 |
| 名稱全域唯一 | API 以名稱定址,呼叫直覺 | 不同父層不能同名 | 允許同名會讓所有名稱呼叫歧義;路徑定址留給 v2 |
| Label 保持精簡 | 套件不用理解業務語意 | 消費端要自建伴生表 | 業務欄位(型別/租戶/權限)套件無法替你把關 |
| 並發戳記非 rowversion | 三資料庫行為一致 | 多一次欄位更新 | 不綁特定資料庫功能 |
從抽象層到上架的具體組成
| 套件 | Cornhsu.Labeling(抽象層)+ Cornhsu.Labeling.EntityFrameworkCore(實作) · MIT |
| 核心手法 | LabelLink<TEntity,TKey> 封閉泛型自動建表 · 泛型主鍵(int / Guid / long / string)反射推斷 |
| 品質 | 78 條測試(SQLite in-memory 跑真外鍵) · Roslyn Analyzer(CHSU001 編譯期警告) · benchmark harness |
| CI | GitHub Actions 矩陣:SQLite / SQL Server / PostgreSQL × EF Core 8 / 9 / 10 |
| 發佈 | OIDC Trusted Publishing(無長效金鑰) · 推 tag 即自動測試、打包、上架 NuGet |
| 掛載 | 標準 DI(AddLabeling) · 無 DI 容器環境(LabelStoreFactory) |
MIT 授權開源。README 含快速開始、Migrations 指南、效能數據與 Limitations;消費端案例 QuillNest 自 v3.1.3 起以本套件驅動標籤系統。