MetalamaConceptual documentationExtending MetalamaAspect weavers
Open sandboxFocusImprove this doc

Aspect weavers

Normal aspects are implemented by the BuildAspect method, which provides advice using the advice factory exposed by the IAspectBuilder interface. Consequently, normal aspects are limited to the capabilities of the IAdviceFactory interface.

In contrast, aspect weavers enable you to perform entirely arbitrary transformations on C# code using the low-level Roslyn API.

When you assign an aspect weaver to an aspect class, Metalama bypasses the BuildAspect method to implement the aspect and instead calls the aspect weaver.

Unlike normal aspects, weaver-based aspects:

  • Are significantly more complex to implement;
  • May significantly impact compilation performance, particularly when many are in use.

Creating a weaver-based aspect

The following steps guide you through the process of creating a weaver-based aspect and its weaver:

Step 1. Reference the Metalama SDK

A weaver project needs to reference the Metalama.Framework.Sdk package privately, and also the regular Metalama.Framework package:

<PackageReference Include="Metalama.Framework.Sdk" Version="$(MetalamaVersion)" PrivateAssets="all" />
<PackageReference Include="Metalama.Framework" Version="$(MetalamaVersion)" />

Step 2. Define the public interface of your aspect (typically an attribute)

Define an aspect class as usual. For example:

using Metalama.Framework.Aspects;

namespace Metalama.Community.Virtuosity;

public class VirtualizeAttribute : TypeAspect { }

Step 3. Create the weaver for this aspect

  1. Add a class that implements the IAspectWeaver interface.
  2. Add the MetalamaPlugInAttribute attribute to this class.

At this point, the code should look like this:

using Metalama.Framework.Engine;
using Metalama.Framework.Engine.AspectWeavers;

namespace Metalama.Community.Virtuosity.Weaver;

[MetalamaPlugIn]
class VirtuosityWeaver : IAspectWeaver
{
    public Task TransformAsync( AspectWeaverContext context )
    {
        throw new NotImplementedException();
    }
}

Step 4. Bind the aspect class to its weaver class

Return to the aspect class and annotate it with a custom attribute of type RequireAspectWeaverAttribute. The constructor argument must point to the weaver class.

[RequireAspectWeaver( typeof(VirtuosityWeaver) )]
public class VirtualizeAttribute : TypeAspect { }

Step 5. Define eligibility of the aspect (optional)

While the BuildAspect method is ignored for weaver aspects, the BuildEligibility method still gets called. This means you can define eligibility in the aspect class as usual, see Defining the eligibility of aspects. For example:

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Eligibility;

namespace Metalama.Community.Virtuosity
{
    [RequireAspectWeaver( typeof(VirtuosityWeaver) )]
    public class VirtualizeAttribute : TypeAspect
    {
        public override void BuildEligibility( IEligibilityBuilder<INamedType> builder )
        {
            base.BuildEligibility( builder );

            builder.MustSatisfy(
                t => t.TypeKind is TypeKind.Class or TypeKind.RecordClass,
                t => $"{t} must be class or a record class" );
        }
    }
}

Step 6. Implement the TransformAsync method

TransformAsync has a parameter of type AspectWeaverContext. This type contains methods for convenient manipulation of the input compilation, namely RewriteAspectTargetsAsync and RewriteSyntaxTreesAsync.

Both methods apply a <xref:Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter> on the input compilation. The difference is that RewriteAspectTargetsAsync only calls Visit on declarations that have the aspect attribute, whereas RewriteSyntaxTreesAsync allows you to modify anything in the entire compilation, but requires more work to identify the relevant declarations.

Note that all methods that apply a <xref:Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter> operate in parallel, which means that your implementation needs to be thread-safe.

For more advanced cases, the Compilation property exposes the input compilation, and your implementation can set this property to the new compilation. The type of the Compilation property is the immutable interface IPartialCompilation. This interface, as well as the extension class PartialCompilationExtensions, offer different methods to transform the compilation. For instance, the RewriteSyntaxTreesAsync method will apply a <xref:Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter> to the input compilation and return the resulting compilation.

For full control of the resulting compilation, the method WithSyntaxTreeTransformations is available.

Remember to write back the context.Compilation property if you're accessing it directly.

Each weaver will be invoked a single time per project, regardless of the number of aspect instances in the project.

The list of aspect instances that your weaver needs to handle is given by the context.AspectInstances property.

To map the Metalama code model to an ISymbol, use the extension methods in SymbolExtensions.

Your weaver does not need to format the output code itself. This task is handled by Metalama at the end of the pipeline, and only when necessary. However, your weaver is responsible for annotating the syntax nodes with the annotations declared in the FormattingAnnotations class.

Example

A simplified version of VirtuosityWeaver could look like this:

// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.

using Metalama.Framework.Engine;
using Metalama.Framework.Engine.AspectWeavers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Threading.Tasks;
using static Microsoft.CodeAnalysis.CSharp.SyntaxKind;

namespace Metalama.Community.Virtuosity;

[MetalamaPlugIn]
public sealed class VirtuosityWeaver : IAspectWeaver
{
    public Task TransformAsync( AspectWeaverContext context ) => context.RewriteAspectTargetsAsync( new Rewriter() );

    private sealed class Rewriter : CSharpSyntaxRewriter
    {
        private static readonly SyntaxKind[] _forbiddenModifiers = { StaticKeyword, SealedKeyword, VirtualKeyword, OverrideKeyword };

        private static readonly SyntaxKind[] _requiredModifiers = { PublicKeyword, ProtectedKeyword, InternalKeyword };

        private static SyntaxTokenList ModifyModifiers( SyntaxTokenList modifiers )
        {
            // Add the virtual modifier.
            if ( !_forbiddenModifiers.Any( modifier => modifiers.Any( modifier ) )
                 && _requiredModifiers.Any( modifier => modifiers.Any( modifier ) ) )
            {
                modifiers = modifiers.Add(
                    SyntaxFactory.Token( VirtualKeyword )
                        .WithTrailingTrivia( SyntaxFactory.ElasticSpace ) );
            }

            return modifiers;
        }

        public override SyntaxNode VisitMethodDeclaration( MethodDeclarationSyntax node ) => node.WithModifiers( ModifyModifiers( node.Modifiers ) );
    }
}

The actual implementation is available on the GitHub repo.

Step 6. Write unit tests

You can test your weaver-based aspect as any other aspect, see Testing the Aspects.

Examples

Available examples of Metalama.Framework.Sdk weavers are: