Skip to main content

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.

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

  1. Metalama is a Roslyn source transformer — it hooks into the compilation pipeline as a compiler plugin
  2. It does NOT use IL rewriting (unlike PostSharp) — it transforms the C# source tree before final compilation
  3. Transformed source is saved to obj/<Configuration>/<TFM>/metalama/ for inspection and debugging
  4. 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 foreach over meta.Target.Parameters was unrolled at compile time into individual lines
  • meta.Target.Method.Name was 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 — for ProcessPayment, 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

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

MistakeCorrection
Forgetting PrivateAssets="all"Always set this — Metalama is compile-time only
Using Debugger.Break() in a templateUse meta.DebugBreak() instead (see Testing & Debugging)
Setting breakpoints in source files of Metalama-transformed projectsBreakpoints in source files may not hit; set them in transformed code under obj/.../metalama/
Using nameof() for introduced membersnameof() resolves at aspect compile-time, not target compile-time; use string literals
Applying aspects to non-partial classes when introducing membersClasses that receive introduced members (via TypeAspect) must be declared partial

Next Steps

Now that you have a working Metalama setup, proceed to:


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.