跳至主要内容

Aspect 基底類別

Metalama 提供數個基底類別,每個都針對特定類型的程式碼轉換而設計。選擇正確的基底類別是建立 Aspect 的第一步。


快速參考

基底類別目標關鍵覆寫使用場景
OverrideMethodAspect方法OverrideMethod()包裝/攔截方法執行
OverrideFieldOrPropertyAspect欄位 / 屬性OverrideProperty攔截屬性 get/set
ContractAspect參數 / 欄位 / 屬性Validate()驗證值(前置條件)
TypeAspect型別(類別、結構)BuildAspect()引入成員、實作介面
MethodAspect方法BuildAspect()程式化方法轉換
FieldOrPropertyAspect欄位 / 屬性BuildAspect()程式化欄位/屬性轉換
EventAspect事件BuildAspect()程式化事件轉換
ConstructorAspect建構函式BuildAspect()程式化建構函式轉換
ParameterAspect參數BuildAspect()程式化參數轉換

OverrideMethodAspect

最常用的基底類別。 它透過包裝原始方法主體來攔截方法執行。

基本結構

public class MyMethodAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// Code before the original method
try
{
var result = meta.Proceed(); // Call original
// Code after the original method (success path)
return result;
}
catch (Exception ex)
{
// Code on exception
throw;
}
finally
{
// Code that always runs (cleanup)
}
}
}

非同步支援

覆寫 OverrideAsyncMethod() 以明確處理非同步:

public override async Task<dynamic?> OverrideAsyncMethod()
{
Console.WriteLine("Before async call");
try
{
var result = await meta.ProceedAsync();
Console.WriteLine("After async call");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"Async error: {ex.Message}");
throw;
}
}

如果你沒有覆寫 OverrideAsyncMethod(),Metalama 會自動將你的 OverrideMethod() 範本包裝在 Task.Run 模式中。

存取方法資訊

public override dynamic? OverrideMethod()
{
// Method metadata (all compile-time)
var className = meta.Target.Type.Name; // "OrderService"
var methodName = meta.Target.Method.Name; // "CalculateTotal"
var returnType = meta.Target.Method.ReturnType; // IType representing decimal
var isAsync = meta.Target.Method.IsAsync; // true/false
var isStatic = meta.Target.Method.IsStatic; // true/false

// Parameter iteration (compile-time unrolled)
foreach (var param in meta.Target.Parameters)
{
var paramName = param.Name; // Compile-time string
var paramValue = param.Value; // Run-time value of the parameter
Console.WriteLine($"{paramName} = {paramValue}");
}

return meta.Proceed();
}

透過屬性設定

Aspect 屬性會成為 Attribute 參數:

public class RetryAttribute : OverrideMethodAspect
{
public int MaxAttempts { get; set; } = 3;
public int DelayMs { get; set; } = 500;
public bool UseExponentialBackoff { get; set; } = true;

public override dynamic? OverrideMethod() { /* use properties */ }
}

// Usage:
[Retry(MaxAttempts = 5, DelayMs = 1000, UseExponentialBackoff = false)]
public void SendEmail() { }

GST 範例:LogAttribute

// From GST.Core.Aspects.Logging.LogAttribute
public class LogAttribute : OverrideMethodAspect
{
public bool LogParameters { get; set; } = true;
public bool LogReturnValue { get; set; } = true;
public bool LogExceptions { get; set; } = true;

public override dynamic? OverrideMethod()
{
var typeName = meta.Target.Type.Name;
var methodName = meta.Target.Method.Name;

AspectLogger.Debug(typeName, $"Entering {methodName}");

if (LogParameters)
{
foreach (var param in meta.Target.Parameters)
{
AspectLogger.Debug(typeName, $" {param.Name} = {param.Value}");
}
}

try
{
var result = meta.Proceed();

if (LogReturnValue && meta.Target.Method.ReturnType.SpecialType != SpecialType.Void)
{
AspectLogger.Debug(typeName, $"Exiting {methodName} with result: {result}");
}

return result;
}
catch (Exception ex)
{
if (LogExceptions)
{
AspectLogger.Error(typeName, $"Exception in {methodName}: {ex.Message}", ex);
}
throw;
}
}

public override async Task<dynamic?> OverrideAsyncMethod()
{
// Similar but with await meta.ProceedAsync()
// ...
}
}

OverrideFieldOrPropertyAspect

攔截欄位或屬性存取。當套用到欄位時,Metalama 會自動將其提升為具有後端欄位的屬性。

基本結構

public class MyPropertyAspect : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get
{
// Code before getting the value
var value = meta.Proceed(); // Get the actual value
// Code after getting the value
return value;
}
set
{
// Code before setting the value
meta.Proceed(); // Actually set the value
// Code after setting the value
}
}
}

實際範例:字串修剪

public class TrimAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
// Trim string values before storing
meta.Target.FieldOrProperty.Value = value?.ToString()?.Trim();
}
}
}

// Usage:
public class UserProfile
{
[Trim]
public string FirstName { get; set; }

[Trim]
public string LastName { get; set; }
}

GST 範例:TrackChanges

// From GST.Core.Aspects.Audit.TrackChangesAttribute
public class TrackChangesAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
var oldValue = meta.Target.FieldOrProperty.Value;
meta.Proceed(); // Set the new value
var newValue = meta.Target.FieldOrProperty.Value;

if (!Equals(oldValue, newValue))
{
var propertyName = meta.Target.FieldOrProperty.Name;
AspectLogger.Information(
meta.Target.Type.Name,
$"[CHANGE] {propertyName}: {oldValue} -> {newValue}");
}
}
}
}

限制

  • ref/out 欄位:與 refout 一起使用的欄位無法提升為屬性
  • 欄位初始值:在提升過程中會被保留
  • 唯讀欄位:只能覆寫 getter

ContractAspect

驗證套用在參數、欄位或屬性上的值。可以把它視為前置條件的強制執行器。

基本結構

public class MyContractAspect : ContractAspect
{
public override void Validate(dynamic? value)
{
if (/* value is invalid */)
{
throw new ArgumentException("Validation failed", meta.Target.Parameter.Name);
}
}
}

value 參數

value 參數代表:

  • 對於輸入參數:呼叫端傳入的參數值
  • 對於輸出參數/回傳值:正在回傳的值
  • 對於屬性/欄位:正在設定的值

實際範例

// Not null validation
public class NotNullAttribute : ContractAspect
{
public override void Validate(dynamic? value)
{
if (value == null)
{
throw new ArgumentNullException(meta.Target.Parameter.Name);
}
}
}

// Range validation
public class RangeAttribute : ContractAspect
{
public double Min { get; set; } = double.MinValue;
public double Max { get; set; } = double.MaxValue;

public override void Validate(dynamic? value)
{
if ((double)value < Min || (double)value > Max)
{
throw new ArgumentOutOfRangeException(
meta.Target.Parameter.Name,
$"Value must be between {Min} and {Max}");
}
}
}

// Not empty validation
public class NotEmptyAttribute : ContractAspect
{
public override void Validate(dynamic? value)
{
if (value is string s && string.IsNullOrWhiteSpace(s))
{
throw new ArgumentException(
"Value cannot be empty or whitespace",
meta.Target.Parameter.Name);
}
}
}

用法

public class RecipeService
{
public void CreateRecipe(
[NotNull][NotEmpty] string name,
[Range(Min = 0, Max = 100)] int temperature)
{
// Parameters are validated BEFORE this line executes
// ...
}
}

屬性上的 Contract

public class Temperature
{
[Range(Min = -273.15, Max = 1000)]
public double Value { get; set; }
}

Contract 方向

Contract 可以強制前置條件(輸入)或後置條件(輸出):

// Precondition: validates input parameter
public void Process([NotNull] string input) { }

// Postcondition: validates return value
[return: NotNull]
public string GetName() { return _name; }

TypeAspect

轉換整個型別 — 程式化地引入成員、實作介面和覆寫現有成員。

基本結構

public class MyTypeAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Programmatically add transformations
builder.IntroduceMethod(nameof(MyMethodTemplate));
builder.ImplementInterface(typeof(IMyInterface));
// ...
}

[Template]
public void MyMethodTemplate()
{
// Template for the introduced method
}
}

引入成員

public class AddToStringAttribute : TypeAspect
{
[Introduce(WhenExists = OverrideStrategy.Override)]
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(meta.Target.Type.Name);
sb.Append(" { ");

foreach (var prop in meta.Target.Type.Properties.Where(p => !p.IsStatic))
{
sb.Append($"{prop.Name} = {prop.Value}, ");
}

sb.Append("}");
return sb.ToString();
}
}

實作介面

public class DisposableAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
builder.ImplementInterface(typeof(IDisposable));
}

[InterfaceMember]
public void Dispose()
{
// Template for IDisposable.Dispose()
foreach (var field in meta.Target.Type.Fields
.Where(f => f.Type.Is(typeof(IDisposable))))
{
field.Value?.Dispose();
}
}
}

GST 範例:NotifyPropertyChanged

// Simplified from GST.Core.Aspects.Observability.NotifyPropertyChangedAttribute
[AttributeUsage(AttributeTargets.Class)]
public class NotifyPropertyChangedAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// 1. Implement INotifyPropertyChanged
builder.ImplementInterface(typeof(INotifyPropertyChanged));

// 2. Override each auto-property's setter
foreach (var property in builder.Target.Properties
.Where(p => !p.IsStatic && p.Writeability == Writeability.All))
{
builder.With(property).Override(nameof(OverridePropertySetter));
}
}

[InterfaceMember]
public event PropertyChangedEventHandler? PropertyChanged;

[Introduce]
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(propertyName));
}

[Template]
public dynamic? OverridePropertySetter
{
get => meta.Proceed();
set
{
if (!Equals(value, meta.Target.FieldOrProperty.Value))
{
meta.Proceed();
OnPropertyChanged(meta.Target.FieldOrProperty.Name);
}
}
}
}

重要:使用引入成員的 TypeAspect 時,目標類別必須宣告為 partial


MethodAspect

OverrideMethodAspect 類似,但完全使用程式化 API:

public class MyMethodAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
builder.Override(nameof(Template));
}

[Template]
public dynamic? Template()
{
Console.WriteLine($"Intercepted: {meta.Target.Method.Name}");
return meta.Proceed();
}
}

何時使用 MethodAspect vs OverrideMethodAspect

特性OverrideMethodAspectMethodAspect
簡潔性較簡單(只需覆寫一個方法)較冗長
多重範本
條件式 Advice有限BuildAspect() 中完全控制
引入成員是(透過 builder.IntroduceMethod()
回報診斷資訊是(透過 builder.Diagnostics

選擇正確的基底類別

使用此決策樹:

你想要轉換什麼?

├── 方法的行為 → 你需要 BuildAspect() 嗎?
│ ├── 否 → OverrideMethodAspect ✅(最簡單)
│ └── 是 → MethodAspect

├── 屬性/欄位值 → 你需要 BuildAspect() 嗎?
│ ├── 否 → OverrideFieldOrPropertyAspect ✅
│ └── 是 → FieldOrPropertyAspect

├── 驗證輸入/輸出值 → ContractAspect ✅

├── 為型別新增成員 → TypeAspect ✅

├── 實作介面 → TypeAspect ✅

└── 程式化地將 Aspect 套用到多個目標 → Fabric(參見下一章)

總結

基底類別簡潔性功能性最適合
OverrideMethodAspect⭐⭐⭐⭐⭐日誌記錄、重試、計時、快取
ContractAspect⭐⭐⭐輸入驗證
OverrideFieldOrPropertyAspect⭐⭐⭐⭐⭐值轉換、變更追蹤
TypeAspect⭐⭐⭐INotifyPropertyChanged、IDisposable、ToString
MethodAspect⭐⭐⭐⭐⭐複雜的條件式轉換

下一篇Fabric — 批次套用 Aspect,無需個別 Attribute。