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. Normal aspects are limited to the capabilities of the IAdviceFactory interface.

Aspect weavers let you perform 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 and instead calls the aspect weaver.

Warning

The use of weaver-based aspects is discouraged:

  • They're significantly more complex to implement and integrate less well with IDEs.
  • They have a significant performance impact, especially when many are in use.

Creating a weaver-based aspect

The following steps guide you through 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, in addition to 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 (optional)

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

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

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

namespace Metalama.Community.Virtuosity;

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

        builder.MustSatisfy(
            t => t.TypeKind is TypeKind.Class,
            t => $"{t} must be 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: RewriteAspectTargetsAsync and RewriteSyntaxTreesAsync.

Both methods apply a CSharpSyntaxRewriter to the input compilation. The difference is that RewriteAspectTargetsAsync only calls the Visit method on declarations that have the aspect attribute, whereas RewriteSyntaxTreesAsync lets you modify anything in the entire compilation, but requires more work to identify the relevant declarations.

All methods that apply a CSharpSyntaxRewriter operate in parallel, which means your implementation must be thread-safe.

For advanced cases, the Compilation property exposes the input compilation. Your implementation can set this property to the new compilation. The Compilation property type is the immutable interface IPartialCompilation. This interface and the extension class PartialCompilationExtensions offer different methods to transform the compilation. For instance, RewriteSyntaxTreesAsync applies a CSharpSyntaxRewriter to the input compilation and returns the resulting compilation.

For full control, use WithSyntaxTreeTransformations.

Remember to write back the context.Compilation property when accessing it directly.

Each weaver is invoked once per project, regardless of the number of aspect instances in the project.

The context.AspectInstances property gives the list of aspect instances your weaver needs to handle.

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

Your weaver doesn't need to format the output code. Metalama handles this at the end of the pipeline. However, your weaver must annotate 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 7. Write unit tests

You can test your weaver-based aspect like any other aspect (see Snapshot testing of aspects).

Examples

Examples of Metalama.Framework.Sdk weavers: