跳到主要内容

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 的比較

面向AttributeFabric
粒度每個宣告批量(基於模式)
可見性在目標位置可見集中管理,與目標分離
設定每個實例可依模式套用不同設定
重構需要更新每個位置修改一個查詢即可
可發現性明顯(閱讀程式碼即可看到)較不明顯(需要知道 Fabric 的存在)
可重用性不適用TransitiveFabric 可跨專案

何時使用哪一種

情境建議
少數特定方法需要 AspectAttribute
類別/命名空間/專案中的所有方法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、快取和可觀察性模式。