Fabric:批量套用 Aspect
Fabric 是編譯期進入點,讓你可以套用 Aspect、設定選項、強制執行架構規則,而不需要在每個宣告上放置 Attribute。它們在編譯過程中自動執行。
為什麼需要 Fabric?
假設一個專案有 200 個服務方法。在每一個上面加 [Log] 會是:
- 繁瑣:手動加 200 個 Attribute
- 容易出錯:很容易漏掉一個
- 雜訊:重複的 Attribute 使程式碼變得雜亂
- 難以維護:改變策略需要修改 200 個檔案
Fabric 透過程式化選取目標來解決這個問題:
// Apply [Log] to ALL public methods in ALL services — one line of code
public class LoggingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Service"))
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.AddAspectIfEligible<LogAttribute>();
}
}
Fabric 類型
ProjectFabric
範圍:定義所在的整個專案。
using Metalama.Framework.Fabrics;
// Must be in a top-level class (not nested)
public class MyProjectFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Apply aspects to any declaration in the project
}
}
使用情境:
- 對所有符合模式的方法套用 Aspect
- 設定 Aspect 函式庫選項
- 定義架構規則(例如「Controller 不可直接相依於 Repository」)
TransitiveProjectFabric
範圍:參考包含此 Fabric 之組件的專案。
// In a library project (e.g., MyCompany.Aspects)
public class MyTransitiveFabric : TransitiveProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// This runs in EVERY project that references MyCompany.Aspects
amender
.SelectMany(p => p.Types)
.Where(t => t.Attributes.Any(a => a.Type.Name == "ServiceAttribute"))
.SelectMany(t => t.Methods)
.AddAspectIfEligible<LogAttribute>();
}
}
使用情境:
- 在所有專案間強制執行公司級標準
- 從共用函式庫自動套用 Aspect
- 跨相依專案的架構驗證
NamespaceFabric
範圍:定義所在的命名空間。
namespace MyApp.Services;
// Applies to all types in MyApp.Services
public class ServicesFabric : NamespaceFabric
{
public override void AmendNamespace(INamespaceAmender amender)
{
amender
.SelectMany(ns => ns.Types)
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.AddAspectIfEligible<LogAttribute>();
}
}
TypeFabric
範圍:定義所在的型別(作為巢狀類別)。
public partial class OrderService
{
// Must be a nested private class
private class Fabric : TypeFabric
{
public override void AmendType(ITypeAmender amender)
{
amender
.SelectMany(t => t.Methods)
.Where(m => m.Name.StartsWith("Get"))
.AddAspectIfEligible<CacheAttribute>();
}
}
public Order GetOrder(int id) { /* automatically cached */ }
public List<Order> GetOrders() { /* automatically cached */ }
public void DeleteOrder(int id) { /* NOT cached */ }
}
注意:包含的類別必須是
partial才能讓 TypeFabric 運作。
Fabric 執行時機與順序
Fabric 全部在編譯期執行(與 Aspect Weaver 同一個 Roslyn 編譯流程,但早於 Weaver)。多種 Fabric 同時存在時,Metalama 會依 「由廣到窄」 的範圍順序執行;每個 Fabric 都讀寫同一份 CompilationModel,後執行者看得到前一階段的修改。
各階段觀察重點:
| 階段 | 何時跑 | 對 CompilationModel 能做什麼 | 失敗時的影響 |
|---|---|---|---|
| TransitiveProjectFabric | 每次任何下游專案編譯 | 跨專案套用 Aspect、強制公司級規則 | 整個 build chain 失敗 |
| ProjectFabric | 本專案編譯時 | 對全專案套 Aspect、加 Diagnostic | 本專案 build 失敗 |
| NamespaceFabric | 每個命名空間掃描時 | 限定範圍套 Aspect、設選項 | 該命名空間下宣告無法套 Aspect |
| TypeFabric | 包含型別載入時 | 對單一型別內部成員套 Aspect | 該型別 build 失敗 |
關鍵推論:
- 由廣到窄 → 同一個 Joinpoint 若被多層 Fabric 命中,會疊加多個 Aspect(順序由 Aspect 自行用
Layer/Order決定)。 - Weaver 永遠跑最後 → Fabric 階段只是「決定要做什麼」,真正的 IL 改寫等到所有 Fabric 都跑完才發生。
- 因此在 Fabric 中讀取目標宣告是安全的,但改寫只能透過
AddAspect/ReportDiagnostic/SetOptions三類 API 排程,不能直接動程式碼。
Fabric 查詢 API
Fabric 使用類似 LINQ 的流暢式 API 來選取目標:
選取型別
amender
.SelectMany(p => p.Types) // All types
.Where(t => t.Name.EndsWith("Service")) // Filter by name
.Where(t => t.Accessibility == Accessibility.Public) // Filter by visibility
.Where(t => t.BaseType?.Name == "BaseService") // Filter by base class
.Where(t => t.ImplementedInterfaces.Any( // Filter by interface
i => i.Name == "IRepository"))
選取方法
amender
.SelectMany(p => p.Types)
.SelectMany(t => t.Methods) // All methods
.Where(m => m.Accessibility == Accessibility.Public) // Public only
.Where(m => !m.IsStatic) // Instance only
.Where(m => m.ReturnType.Is(typeof(Task<>))) // Async only
.Where(m => m.Parameters.Count > 0) // Has parameters
選取屬性
amender
.SelectMany(p => p.Types)
.SelectMany(t => t.Properties)
.Where(p => p.Writeability == Writeability.All) // Read-write
.Where(p => p.Type.Is(typeof(string))) // String type
套用 Aspect
// Apply with default settings
.AddAspectIfEligible<LogAttribute>();
// Apply with configuration
.AddAspectIfEligible(m => new CacheAttribute
{
AbsoluteExpirationSeconds = 300
});
// Apply only if the aspect is eligible for the target
.AddAspectIfEligible<RetryAttribute>(); // Won't apply to void methods if not eligible
Fabric 與 Attribute 的比較
| 面向 | Attribute | Fabric |
|---|---|---|
| 粒度 | 每個宣告 | 批量(基於模式) |
| 可見性 | 在目標位置可見 | 集中管理,與目標分離 |
| 設定 | 每個實例 | 可依模式套用不同設定 |
| 重構 | 需要更新每個位置 | 修改一個查詢即可 |
| 可發現性 | 明顯(閱讀程式碼即可看到) | 較不明顯(需要知道 Fabric 的存在) |
| 可重用性 | 不適用 | TransitiveFabric 可跨專案 |
何時使用哪一種
| 情境 | 建議 |
|---|---|
| 少數特定方法需要 Aspect | Attribute |
| 類別/命名空間/專案中的所有方法 | Fabric |
| 每個方法需要不同的設定 | Attribute |
| 專案中統一的策略 | Fabric |
| 跨專案標準 | TransitiveProjectFabric |
| Aspect 函式庫發布 | 兩者並用:Fabric 用於預設值,Attribute 用於覆寫 |
實際範例
範例 1:自動記錄所有 Controller 方法
public class ControllerLoggingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.BaseType?.Name == "ControllerBase" ||
t.Attributes.Any(a => a.Type.Name == "ApiControllerAttribute"))
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public &&
!m.IsStatic)
.AddAspectIfEligible(m => new LogAttribute
{
LogParameters = true,
LogReturnValue = true
});
}
}
範例 2:對所有參數強制執行非 Null 檢查
public class NullCheckFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.Accessibility == Accessibility.Public)
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.SelectMany(m => m.Parameters)
.Where(p => p.Type.IsNullable != true &&
p.Type.IsReferenceType == true)
.AddAspectIfEligible<NotNullAttribute>();
}
}
範例 3:為 Repository 模式設定快取
public class CachingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Cache all "Get" methods in repositories for 5 minutes
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Repository"))
.SelectMany(t => t.Methods)
.Where(m => m.Name.StartsWith("Get") &&
m.ReturnType.SpecialType != SpecialType.Void)
.AddAspectIfEligible(m => new CacheAttribute
{
AbsoluteExpirationSeconds = 300
});
}
}
使用 Fabric 進行架構驗證
Fabric 也可以強制執行架構規則(不需要 Metalama.Extensions.Architecture):
public class ArchitectureFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Warn if a Controller directly references a Repository
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Controller"))
.SelectMany(t => t.Fields)
.Where(f => f.Type.Name.EndsWith("Repository"))
.ReportDiagnostic(f =>
DiagnosticDescriptor.Create(
"ARCH001",
"Controllers should not directly reference repositories",
DiagnosticSeverity.Warning)
.WithMessage($"Controller uses {f.Type.Name} directly. Use a service layer."));
}
}
GST 框架:目前狀態
GST 框架目前僅使用 Attribute(不使用 Fabric)。這是一個有意識的設計選擇:
| 原因 | 說明 |
|---|---|
| 明確控制 | 每個 Aspect 的套用在目標位置都是可見的 |
| 細粒度設定 | 不同的方法需要不同的 Aspect 參數 |
| 應用層彈性 | 應用程式可以選擇要套用哪些 Aspect |
潛在改進:TransitiveProjectFabric 可以自動對所有公開方法參數套用 [NotNull],減少應用專案中的樣板程式碼。
總結
| Fabric 類型 | 範圍 | 自動執行? | 使用情境 |
|---|---|---|---|
ProjectFabric | 當前專案 | 是 | 專案層級策略 |
TransitiveProjectFabric | 參考此組件的專案 | 是 | 跨專案標準 |
NamespaceFabric | 單一命名空間 | 是 | 命名空間層級規則 |
TypeFabric | 單一型別(巢狀) | 是 | 型別層級自動化 |
下一步:模式函式庫 — 內建的 Contract、快取和可觀察性模式。