メインコンテンツまでスキップ

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) │
└──────────────────────┘

重點

  1. Metalama 是一個 Roslyn 原始碼轉換器 — 它以編譯器外掛的方式掛接到編譯管線中
  2. 它不使用 IL 改寫(不同於 PostSharp)— 它在最終編譯之前轉換 C# 語法樹
  3. 轉換後的原始碼會儲存obj/<Configuration>/<TFM>/metalama/,供檢視與除錯使用
  4. 來自 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.Parametersforeach 迴圈在編譯時期被展開為個別的程式行
  • 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 的運作方式、生命週期與組合。