Metalama 快速入門
本章將引導你在 .NET 專案中設定 Metalama,並建立你的第一個 Aspect。
先決條件
- .NET SDK 8.0 或更新版本
- Visual Studio 2022 (17.8+) 或 JetBrains Rider (2023.3+)
- 基本熟悉 C# Attribute 與 NuGet 套件
安裝
步驟 1:新增 NuGet 套件
透過 CLI 或 Visual Studio 將 Metalama 加入你的專案:
dotnet add package Metalama.Framework
或加入你的 .csproj:
<PackageReference Include="Metalama.Framework" Version="2025.1.17" PrivateAssets="all" />
重要:
PrivateAssets="all"屬性至關重要。它告訴 NuGet,Metalama 是僅限編譯時期的相依性。它不會隨你的組件一同發佈,也不會暴露給使用端專案。
步驟 2:集中式套件管理(建議)
對於多專案方案,使用 Directory.Packages.props 來集中管理版本:
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Metalama.Framework" Version="2025.1.17" />
</ItemGroup>
</Project>
然後在每個 .csproj 中:
<PackageReference Include="Metalama.Framework" PrivateAssets="all" />
這就是 GST 框架管理其 Metalama 相依性的方式。
步驟 3:驗證安裝
建立一個簡單的 Aspect 來驗證一切是否正常運作:
using Metalama.Framework.Aspects;
using System;
public class HelloAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Console.WriteLine("Hello from Metalama!");
return meta.Proceed();
}
}
套用它:
public class MyService
{
[HelloAspect]
public void DoWork()
{
Console.WriteLine("Doing work...");
}
}
建置並執行。你應該會看到:
Hello from Metalama!
Doing work...
了解編譯管線
當你建置一個包含 Metalama 的專案時,編譯會依循以下管線:
Source Code (.cs files)
│
▼
┌──────────────────────┐
│ Roslyn C# Compiler │
│ (normal compilation) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Metalama Plugin │
│ │
│ 1. Discover aspects │
│ 2. Run fabrics │
│ 3. Evaluate │
│ eligibility │
│ 4. Execute │
│ BuildAspect() │
│ 5. Expand templates │
│ 6. Merge into │
│ syntax tree │
│ 7. Report │
│ diagnostics │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Transformed Code │
│ (saved to obj/ │
│ .../metalama/) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Final IL Assembly │
│ (.dll / .exe) │
└──────────────────────┘
重點
- Metalama 是一個 Roslyn 原始碼轉換器 — 它以編譯器外掛的方式掛接到編譯管線中
- 它不使用 IL 改寫(不同於 PostSharp)— 它在最終編譯之前轉換 C# 語法樹
- 轉換後的原始碼會儲存到
obj/<Configuration>/<TFM>/metalama/,供檢視與除錯使用 - 來自 Aspect 的建置錯誤會以一般編譯器錯誤/警告的形式顯示在你的 IDE 中
你的第一個 Aspect:逐步指引
讓我們從頭建立一個實用的日誌記錄 Aspect。
步驟 1:定義 Aspect 類別
using Metalama.Framework.Aspects;
using System;
using System.Diagnostics;
public class LogAttribute : OverrideMethodAspect
{
/// <summary>
/// Whether to include method parameters in the log output.
/// </summary>
public bool LogParameters { get; set; } = true;
public override dynamic? OverrideMethod()
{
// 1. Log method entry
var methodName = meta.Target.Method.Name;
Console.WriteLine($">> Entering {methodName}");
// 2. Optionally log parameters
if (LogParameters)
{
foreach (var param in meta.Target.Parameters)
{
Console.WriteLine($" {param.Name} = {param.Value}");
}
}
try
{
// 3. Call the original method
var result = meta.Proceed();
// 4. Log the result
Console.WriteLine($"<< Exiting {methodName} with result: {result}");
return result;
}
catch (Exception ex)
{
// 5. Log exceptions
Console.WriteLine($"!! Exception in {methodName}: {ex.Message}");
throw;
}
}
}
步驟 2:套用到方法
public class OrderService
{
[Log]
public decimal CalculateTotal(decimal price, int quantity)
{
return price * quantity;
}
[Log(LogParameters = false)]
public void ProcessPayment(string creditCardNumber)
{
// LogParameters = false to avoid logging sensitive data
// ... payment processing logic
}
}
步驟 3:了解產生的程式碼
當你建置時,Metalama 會將 CalculateTotal 轉換成類似以下的內容:
// Generated code (visible in obj/.../metalama/OrderService.cs)
public decimal CalculateTotal(decimal price, int quantity)
{
Console.WriteLine(">> Entering CalculateTotal");
Console.WriteLine($" price = {price}");
Console.WriteLine($" quantity = {quantity}");
try
{
decimal result;
result = price * quantity;
Console.WriteLine($"<< Exiting CalculateTotal with result: {result}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"!! Exception in CalculateTotal: {ex.Message}");
throw;
}
}
請注意:
- 對
meta.Target.Parameters的foreach迴圈在編譯時期被展開為個別的程式行 meta.Target.Method.Name被替換為字串常值"CalculateTotal"meta.Proceed()被替換為原始方法主體(price * quantity)if (LogParameters)在編譯時期被求值 — 對於ProcessPayment,參數記錄的程式行完全不會產生
步驟 4:檢視轉換後的程式碼
你可以在 IDE 或檔案系統中檢視產生的程式碼:
obj/
└── Debug/
└── net8.0/
└── metalama/
└── OrderService.cs ← Transformed source
這對於理解和除錯 Aspect 行為非常有價值。
專案結構建議
Aspect 程式庫(建議)
對於可重複使用的 Aspect,建立一個專屬的類別庫:
MySolution/
├── MyCompany.Aspects/ ← Aspects live here
│ ├── Logging/
│ │ └── LogAttribute.cs
│ ├── Caching/
│ │ └── CacheAttribute.cs
│ └── MyCompany.Aspects.csproj
├── MyCompany.Core/ ← Core logic uses aspects
│ └── MyCompany.Core.csproj ← References MyCompany.Aspects
└── MyCompany.App/ ← Application entry point
└── MyCompany.App.csproj
這正是 GST 框架所使用的模式:
GST/
├── GST.Core.Aspects/ ← All 32 aspects
├── GST.Core.Abstractions/ ← Service interfaces (ILoggerService, etc.)
├── GST.Core/ ← Core framework (uses aspects)
└── GST.ServiceInfrastructure/ ← Application host (uses aspects)
關鍵原則
Aspect 應該放在自己的專案中,以便:
- 跨多個專案重複使用
- 獨立測試
- 獨立版本控管
常見初始錯誤
| 錯誤 | 修正方式 |
|---|---|
忘記加 PrivateAssets="all" | 務必設定此屬性 — Metalama 僅在編譯時期使用 |
在 Template 中使用 Debugger.Break() | 改用 meta.DebugBreak()(參見 Testing & Debugging) |
| 在 Metalama 轉換專案的原始檔中設定中斷點 | 原始檔中的中斷點可能無法命中;請在 obj/.../metalama/ 下的轉換後程式碼中設定 |
對引入的成員使用 nameof() | nameof() 在 Aspect 編譯時期解析,而非目標編譯時期;請使用字串常值 |
在引入成員時將 Aspect 套用到非 partial 類別 | 接收引入成員的類別(透過 TypeAspect)必須宣告為 partial |
下一步
現在你已經有了可運作的 Metalama 設定,請繼續閱讀:
快速參考
// Minimal aspect template
using Metalama.Framework.Aspects;
public class MyAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// Code before the original method
var result = meta.Proceed(); // Call original
// Code after the original method
return result;
}
}
// Apply with attribute
[MyAspect]
public void TargetMethod() { }
下一篇:核心概念 — Aspect 的運作方式、生命週期與組合。