返回作品列表
開源套件 · .NET / EF Core NuGet 已上架

Cornhsu.Labeling

EF Core 多型標籤系統 · 抽象層+實作雙套件

EF Core 沒有內建多型關聯——「標籤 A 貼在實體 B 上」的 B 可能是筆記、待辦、行事曆,每個專案都得土法煉鋼一次。我把自己桌面應用的標籤功能抽象成公開套件:一行註冊,連結表自動生成,每條連結都有真外鍵,刪除永遠不留孤兒資料。

類型
.NET 類別庫(NuGet 雙套件)
版本
v1.0.0 · MIT 授權
相容性
EF Core 8 / 9 / 10
資料庫
SQLite · SQL Server · PostgreSQL
測試
78 條(72 核心+6 Analyzer)
發佈管線
GitHub Actions · OIDC Trusted Publishing
Program.cs — 一行註冊,連結表自動生成
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
技術亮點

六個值得一提的設計

1

封閉泛型自動建表

核心手法是 LabelLink<TEntity,TKey> 封閉泛型——每個註冊型別由 EF 自動生成一張帶真外鍵的連結表(每型別一表)。孤兒資料在資料庫層就不可能出現,而不是靠應用程式紀律。

2

泛型主鍵、單一型別參數 API

主鍵型別(int / Guid / long / string)在註冊時以反射從 ILabelable<TKey> 推斷,使用者永遠只寫 r.Labelable<Note>();跨型別查詢結果以 EntityIdAs<TKey>() 取回強型別。

3

內建 Roslyn Analyzer

實作了 ILabelable<TKey> 卻忘記註冊 → 編譯期警告(CHSU001),不用等執行期才炸;隨套件出貨,安裝即生效。

4

並發保護不綁資料庫

ConcurrencyStamp 由 store 輪換而非依賴 rowversion,三個資料庫行為完全一致;兩邊同時修改同一標籤時,後存檔者得到明確例外而非默默覆蓋。

5

效能先量再優化

寫了 benchmark harness 實測後,保留樸素的每型別一次查詢(5 型別 × 1 萬筆 ≈ 19ms,夠用),把力氣花在真正的瓶頸——清單畫面 N+1(批次 API 實測 、省 49 次 roundtrip)。

6

兩種掛載方式

標準 DI(AddLabeling)之外,為無 DI 容器的 WPF / WinForms 應用提供 LabelStoreFactory——這是真實遷移時發現的缺口,而不是想像出來的需求。

工程方法

規畫書 → 里程碑 → 真實產品驗收 → 才定版

M1–M2

抽象與實作

先寫規畫書拍板設計取捨(每型別一表 vs discriminator vs TPH、名稱全域唯一 vs 路徑定址),測試用 SQLite in-memory 跑真外鍵,不用 EF InMemory——它不執行外鍵約束,測不到核心保證。

M3

發佈管線

Trusted Publishing(OIDC 換一小時臨時金鑰),repo 裡沒有任何秘密。

M4 ★

真實驗證(不可跳過)

把自家應用 QuillNest 的自製標籤系統整個刪掉改用套件——包含使用者正式資料的搬移 migration(int→Guid 對照、先在副本演練、備援升級路徑)。遷移過程真實地暴露了兩個 API 缺口(無 DI 入口、批次讀),都以正門補上而非開後門。

v1.0 前置

Schema 決定全部關帳

效能基準、三資料庫 × 三 EF 版本矩陣、並發語意定案——schema 影響的決定全部在 1.0 前做完,因為之後就是 breaking change。

M5

打磨

Analyzer、批次貼標、可選 ILogger。全部完成後才 tag v1.0.0,API 穩定承諾生效。

真實驗證

先讓自己的產品當第一個使用者

1.0 定版前,QuillNest(WPF 個人生產力應用)的標籤系統整個換成本套件驅動——標籤管理、行事曆分類都跑在 Cornhsu.Labeling 上,包含使用者正式資料的搬移。套件的業務欄位(顏色、圖示、類型)由消費端伴生表實作,正是「Label 保持精簡」取捨的實際示範。

QuillNest 標籤管理介面:標籤列表含顏色、圖示、類型與使用次數,由 Cornhsu.Labeling 驅動
QuillNest 標籤管理 · 顏色/圖示/類型為消費端伴生欄位
QuillNest 行事曆以標籤分類事件的實際畫面
行事曆分類實戰 · 在使用者正式資料上跑通才定版
設計取捨

取了什麼、捨了什麼、為什麼

決定理由
每型別一張連結表真外鍵、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
CIGitHub Actions 矩陣:SQLite / SQL Server / PostgreSQL × EF Core 8 / 9 / 10
發佈OIDC Trusted Publishing(無長效金鑰) · 推 tag 即自動測試、打包、上架 NuGet
掛載標準 DI(AddLabeling) · 無 DI 容器環境(LabelStoreFactory