進階主題
本章涵蓋 Metalama 進階功能,適合想建立精密 Aspect 的開發者。
使用 BuildAspect 的命令式 Advising
BuildAspect() 方法讓你完全以程式碼控制 Aspect 的行為。不只是覆寫 Template,你還可以:
- 內省目標宣告
- 條件式新增或略過轉換
- 引入新成員
- 實作介面
- 回報診斷(錯誤/警告)
- 透過 Tag 傳遞資料給 Template
基本模式
public class MyAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Introspect the target
var method = builder.Target;
// Conditionally apply
if (method.Parameters.Count > 0)
{
builder.Override(nameof(LoggedTemplate));
}
else
{
builder.Override(nameof(SimpleTemplate));
}
}
[Template]
public dynamic? LoggedTemplate() { /* ... */ }
[Template]
public dynamic? SimpleTemplate() { /* ... */ }
}
透過 Tag 傳遞資料
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Compute something at compile time
var sensitiveParams = builder.Target.Parameters
.Where(p => p.Attributes.Any(a => a.Type.Name == "SensitiveAttribute"))
.Select(p => p.Name)
.ToList();
// Pass to template
builder.Override(nameof(Template),
tags: new { SensitiveParams = sensitiveParams });
}
[Template]
public dynamic? Template()
{
var sensitive = (List<string>)meta.Tags["SensitiveParams"]!;
foreach (var param in meta.Target.Parameters)
{
if (sensitive.Contains(param.Name))
Console.WriteLine($" {param.Name} = [REDACTED]");
else
Console.WriteLine($" {param.Name} = {param.Value}");
}
return meta.Proceed();
}
引入成員
Aspect 可以為目標型別新增成員。
宣告式引入
在 Aspect 成員上使用 [Introduce]:
public class TimestampAspect : TypeAspect
{
[Introduce]
public DateTime CreatedAt { get; } = DateTime.UtcNow;
[Introduce]
public DateTime? ModifiedAt { get; set; }
[Introduce]
public void Touch()
{
ModifiedAt = DateTime.UtcNow;
}
}
程式化引入
使用 BuildAspect() 進行動態成員引入:
public class CloneableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Introduce a Clone method
builder.IntroduceMethod(nameof(CloneTemplate),
buildMethod: m => m.Name = "Clone");
}
[Template]
public object CloneTemplate()
{
var clone = meta.Target.Type.Constructors
.First(c => c.Parameters.Count == 0)
.Invoke();
foreach (var prop in meta.Target.Type.Properties
.Where(p => p.Writeability == Writeability.All && !p.IsStatic))
{
prop.With(clone).Value = prop.With(meta.This).Value;
}
return clone;
}
}
覆寫策略
控制成員已存在時的行為:
[Introduce(WhenExists = OverrideStrategy.Override)] // Replace existing
[Introduce(WhenExists = OverrideStrategy.Ignore)] // Skip if exists
[Introduce(WhenExists = OverrideStrategy.New)] // Add with 'new' keyword
[Introduce(WhenExists = OverrideStrategy.Fail)] // Report error (default)
實作介面
TypeAspect 可以讓目標型別實作介面:
public class EquatableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Only implement if not already implemented
if (!builder.Target.ImplementedInterfaces.Any(
i => i.Name == "IEquatable"))
{
builder.ImplementInterface(typeof(IEquatable<>)
.MakeGenericType(builder.Target.ToType()));
}
}
[InterfaceMember]
public bool Equals(dynamic? other)
{
if (other == null || other.GetType() != meta.This.GetType())
return false;
foreach (var prop in meta.Target.Type.Properties
.Where(p => !p.IsStatic))
{
if (!Equals(prop.With(meta.This).Value, prop.With(other).Value))
return false;
}
return true;
}
}
介面成員 Template
[InterfaceMember] 標記成員為介面成員的實作:
public class DisposableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
builder.ImplementInterface(typeof(IDisposable));
}
[InterfaceMember]
public void Dispose()
{
// Auto-dispose all IDisposable fields
foreach (var field in meta.Target.Type.Fields
.Where(f => f.Type.Is(typeof(IDisposable)) && !f.IsStatic))
{
((IDisposable?)field.Value)?.Dispose();
}
}
}
適用性
適用性定義了 Aspect 可以合法套用在哪些宣告上。當 Aspect 被套用到不符合適用性的目標時,Metalama 會回報編譯錯誤。
定義適用性
覆寫 BuildEligibility():
public class CacheAttribute : OverrideMethodAspect
{
public override void BuildEligibility(IEligibilityBuilder builder)
{
base.BuildEligibility(builder);
// Must not be void (nothing to cache)
builder.ReturnType().MustNotBe(typeof(void));
// Must not be static (instance-level cache)
builder.MustNotBeStatic();
// Must not be abstract
builder.MustNotBeAbstract();
}
public override dynamic? OverrideMethod() { /* ... */ }
}
自訂適用性條件
public override void BuildEligibility(IEligibilityBuilder builder)
{
builder.MustSatisfy(
method => method.Parameters.Count > 0,
method => $"{method} must have at least one parameter for cache key generation"
);
builder.MustSatisfy(
method => !method.ReturnType.Is(typeof(void)),
method => $"{method} must have a return value to cache"
);
}
適用性 vs. 診斷
| 功能 | 適用性 | 診斷 |
|---|---|---|
| 用途 | 「此 Aspect 無法在此處運作」 | 「此 Aspect 可以運作,但有問題」 |
| 效果 | 阻止 Aspect 套用 | 建置期間產生警告/錯誤 |
| IDE | Aspect 不會出現在快速修正選單 | Aspect 可以套用,顯示警告 |
| 範例 | [Cache] 用在 void 方法上 | [Cache] 用在參數不可雜湊的方法上 |
診斷
從 Aspect 回報自訂警告與錯誤:
定義診斷
public class MyAspect : OverrideMethodAspect
{
// Define diagnostic descriptors
private static readonly DiagnosticDefinition<IMethod> _warning =
new("MY001", Severity.Warning,
"Method '{0}' has too many parameters ({1}). Consider refactoring.");
private static readonly DiagnosticDefinition<IParameter> _error =
new("MY002", Severity.Error,
"Parameter '{0}' of type '{1}' is not serializable.");
}
回報診斷
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Report warning
if (builder.Target.Parameters.Count > 5)
{
builder.Diagnostics.Report(
_warning.WithArguments(builder.Target, builder.Target.Parameters.Count));
}
// Report error (prevents compilation)
foreach (var param in builder.Target.Parameters)
{
if (!IsSerializable(param.Type))
{
builder.Diagnostics.Report(
_error.WithArguments(param, param.Type));
}
}
// Suppress existing diagnostics
builder.Diagnostics.Suppress(
new SuppressionDefinition("CS0067")); // Suppress "event never used"
}
Aspect 組合模式
Aspect 聚合
一個 Aspect 可以加入其他 Aspect:
public class ServiceMethodAttribute : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// This single attribute adds multiple aspects
builder.Outbound.AddAspect<LogAttribute>();
builder.Outbound.AddAspect(new RetryAttribute { MaxAttempts = 3 });
builder.Outbound.AddAspect<TimingAttribute>();
}
}
// Usage: one attribute instead of three
[ServiceMethod]
public async Task ProcessOrderAsync(Order order) { ... }
Aspect 繼承
Aspect 可以繼承自其他 Aspect:
public class DetailedLogAttribute : LogAttribute
{
public override dynamic? OverrideMethod()
{
Console.WriteLine($"[{DateTime.UtcNow:O}] Thread: {Thread.CurrentThread.ManagedThreadId}");
return base.OverrideMethod(); // Call base aspect template
}
}
條件式 Aspect 套用
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Only apply if method is in a specific namespace
if (builder.Target.DeclaringType.Namespace.StartsWith("MyApp.Services"))
{
builder.Override(nameof(Template));
}
else
{
builder.SkipAspect(); // Don't apply to this target
}
}
以程式碼新增 Contract
你可以從 BuildAspect() 中新增參數驗證:
public class ValidateInputAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
foreach (var param in builder.Target.Parameters)
{
if (param.Type.IsReferenceType == true &&
param.Type.IsNullable != true)
{
// Add NotNull contract to all non-nullable reference parameters
builder.With(param).AddContract(
nameof(NotNullTemplate),
ContractDirection.Input);
}
}
}
[Template]
public void NotNullTemplate(dynamic? value)
{
if (value == null)
{
throw new ArgumentNullException(meta.Target.Parameter.Name);
}
}
}
建構函式初始化
將程式碼加入建構函式:
public class InitializeFieldsAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Add initialization code to all constructors
builder.AddInitializer(nameof(InitTemplate),
InitializerKind.BeforeInstanceConstructor);
}
[Template]
public void InitTemplate()
{
Console.WriteLine($"Creating instance of {meta.Target.Type.Name}");
}
}
摘要
| 功能 | API | 使用情境 |
|---|---|---|
BuildAspect() | 命令式 Advising | 複雜的條件式轉換 |
meta.Tags | 資料傳遞 | 編譯期計算資料傳給 Template |
[Introduce] | 宣告式成員引入 | 新增欄位、屬性、方法 |
IntroduceMethod() | 程式化引入 | 動態成員產生 |
ImplementInterface() | 介面實作 | 新增 IDisposable、INPC 等 |
BuildEligibility() | 適用性規則 | 限制 Aspect 目標 |
Diagnostics.Report() | 自訂警告/錯誤 | 引導使用者、強制規則 |
Outbound.AddAspect() | Aspect 聚合 | 組合多個 Aspect |
AddContract() | 程式化 Contract | 動態驗證 |
AddInitializer() | 建構函式注入 | 初始化程式碼 |
下一篇:測試與除錯 — 如何測試與除錯 Aspect。