Getting Started with Metalama
This chapter walks you through setting up Metalama in a .NET project and creating your first aspect.
Prerequisites
- .NET SDK 8.0 or later
- Visual Studio 2022 (17.8+) or JetBrains Rider (2023.3+)
- Basic familiarity with C# attributes and NuGet packages
Installation
Step 1: Add the NuGet Package
Add Metalama to your project via the CLI or Visual Studio:
dotnet add package Metalama.Framework
Or add to your .csproj:
<PackageReference Include="Metalama.Framework" Version="2025.1.17" PrivateAssets="all" />
Important: The
PrivateAssets="all"attribute is critical. It tells NuGet that Metalama is a compile-time only dependency. It will not be shipped with your assembly or exposed to consuming projects.
Step 2: Central Package Management (Recommended)
For multi-project solutions, use Directory.Packages.props to centralize the version:
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Metalama.Framework" Version="2025.1.17" />
</ItemGroup>
</Project>
Then in each .csproj:
<PackageReference Include="Metalama.Framework" PrivateAssets="all" />
This is how the GST framework manages its Metalama dependency.
Step 3: Verify Installation
Create a simple aspect to verify everything works:
using Metalama.Framework.Aspects;
using System;
public class HelloAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Console.WriteLine("Hello from Metalama!");
return meta.Proceed();
}
}
Apply it:
public class MyService
{
[HelloAspect]
public void DoWork()
{
Console.WriteLine("Doing work...");
}
}
Build and run. You should see:
Hello from Metalama!
Doing work...
Understanding the Compilation Pipeline
When you build a project with Metalama, the compilation follows this pipeline:
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) │
└──────────────────────┘
Key Points
- Metalama is a Roslyn source transformer — it hooks into the compilation pipeline as a compiler plugin
- It does NOT use IL rewriting (unlike PostSharp) — it transforms the C# source tree before final compilation
- Transformed source is saved to
obj/<Configuration>/<TFM>/metalama/for inspection and debugging - Build errors from aspects appear as normal compiler errors/warnings in your IDE
Your First Aspect: Step by Step
Let's build a practical logging aspect from scratch.
Step 1: Define the Aspect Class
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;
}
}
}
Step 2: Apply to a Method
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
}
}
Step 3: Understand What Gets Generated
When you build, Metalama transforms CalculateTotal into something like:
// 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;
}
}
Notice:
- The
foreachovermeta.Target.Parameterswas unrolled at compile time into individual lines meta.Target.Method.Namewas replaced with the string literal"CalculateTotal"meta.Proceed()was replaced with the original method body (price * quantity)- The
if (LogParameters)was evaluated at compile time — forProcessPayment, the parameter logging lines are not generated at all
Step 4: View the Transformed Code
You can inspect the generated code in your IDE or file system:
obj/
└── Debug/
└── net8.0/
└── metalama/
└── OrderService.cs ← Transformed source
This is invaluable for understanding and debugging aspect behavior.
Project Structure Recommendations
Aspect Library (Recommended)
For reusable aspects, create a dedicated class library:
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
This is exactly the pattern used by the GST framework:
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)
Key Principle
Aspects should be in their own project so they can be:
- Reused across multiple projects
- Tested independently
- Versioned separately
Common Initial Mistakes
| Mistake | Correction |
|---|---|
Forgetting PrivateAssets="all" | Always set this — Metalama is compile-time only |
Using Debugger.Break() in a template | Use meta.DebugBreak() instead (see Testing & Debugging) |
| Setting breakpoints in source files of Metalama-transformed projects | Breakpoints in source files may not hit; set them in transformed code under obj/.../metalama/ |
Using nameof() for introduced members | nameof() resolves at aspect compile-time, not target compile-time; use string literals |
Applying aspects to non-partial classes when introducing members | Classes that receive introduced members (via TypeAspect) must be declared partial |
Next Steps
Now that you have a working Metalama setup, proceed to:
- Core Concepts — Understand how aspects work internally
- T# Template Language — Master the compile-time template system
- GST Examples — See production-ready aspects in action
Quick Reference
// 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() { }
Next: Core Concepts — How aspects work, lifecycle, and composition.