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欄位:與ref或out一起使用的欄位無法提升為屬性- 欄位初始值:在提升過程中會被保留
- 唯讀欄位:只能覆寫 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:
| 特性 | OverrideMethodAspect | MethodAspect |
|---|---|---|
| 簡潔性 | 較簡單(只需覆寫一個方法) | 較冗長 |
| 多重範本 | 否 | 是 |
| 條件式 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。