MetalamaConceptual documentationCreating aspectsExposing configuration
Open sandboxFocusImprove this doc

Exposing configuration

Complex and widely-used aspects often require a centralized, project-wide method for configuring their compile-time behavior.

There are two complementary mechanisms for configuration: MSBuild properties and the configuration API.

Benefits

  • Centralized aspect options. Providing a configuration API allows the entire project to be configured from a single location. Without a configuration API, users must supply the configuration each time a custom attribute is used.

  • Debug/Release-aware options. Without a configuration API, setting options based on the Debug/Release build configuration can be challenging.

  • Run-time performance. Decisions made at compile time and the generation of optimal run-time code can enhance the run-time performance of your application.

Consuming MSBuild properties

The simplest method for an aspect to accept a configuration property is through reading an MSBuild property using the IProject.TryGetProperty method. MSBuild properties are not visible to aspects by default: you must instruct MSBuild to pass it to the compiler using the CompilerVisibleProperty item.

We recommend the following approach to consume a configuration property:

  1. Create a file named YourProject.targets (the actual file name is not important, but the extension is):

    <Project>
        <ItemGroup>
            <CompilerVisibleProperty Include="YourProperty" />
        </ItemGroup>
    </Project>
    
  2. Include YourProject.targets in your project and mark it for inclusion in the build directory of your NuGet package. This ensures that the property will be visible to the aspect for all projects referencing your package. Your csproj file should look like this:

    <Project Sdk="Microsoft.NET.Sdk">
        <!-- ... -->
        <ItemGroup>
            <None Include="YourProject.targets">
                <Pack>true</Pack>
                <PackagePath>build</PackagePath>
            </None>
        </ItemGroup>
        <!-- ... -->
    </Project>
    
  3. To configure the aspect, users should set this property in the csproj file, as shown in the following snippet:

    <Project Sdk="Microsoft.NET.Sdk">
        <!-- ... -->
        <PropertyGroup>
            <YourProperty>TheValue</YourProperty>
        </PropertyGroup>
        <!-- ... -->
    </Project>
    
    Warning

    Line breaks and semicolons are not allowed in the values of compiler-visible properties as they will cause your aspect to receive an incorrect value.

Example

In the example below, the Log aspect reads the default category from the MSBuild property.

Source Code
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.ConsumingProperty
5{
6    public class Log : OverrideMethodAspect


7    {
8        public string? Category { get; set; }
9
10        public override dynamic? OverrideMethod()
11        {
12            if ( !meta.Target.Project.TryGetProperty( "DefaultLogCategory", out var defaultCategory ) )
13            {
14                defaultCategory = "Default";
15            }
16
17            Console.WriteLine( $"{this.Category ?? defaultCategory}: Executing {meta.Target.Method}." );
18
19            return meta.Proceed();
20        }
21    }
22}
Transformed Code
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.ConsumingProperty
5{
6
7#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
8    public class Log : OverrideMethodAspect
9    {
10        public string? Category { get; set; }
11
12        public override dynamic? OverrideMethod() => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
13
14    }



15
16#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
17
18}

Exposing a configuration API

For more complex aspects, a set of MSBuild properties may not suffice. In such cases, you can construct a configuration API for your users to call from their project fabrics.

To create a configuration API:

  1. Create a class that inherits from ProjectExtension and has a default constructor.
  2. Optionally, override the Initialize method, which receives the IProject.
  3. In your aspect code, call the IProject.Extension<T>() method, where T is your configuration class, to get the configuration object.
  4. Optionally, create an extension method for the IProject type to expose your configuration API, making it more discoverable. The class must be annotated with [CompileTime].
  5. To configure your aspect, users should implement a project fabric and access your configuration API using this extension method.

Example

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.AspectConfiguration
5{
6    // The aspect itself, consuming the configuration.
7    public class LogAttribute : OverrideMethodAspect
8    {
9        public string? Category { get; set; }
10
11        public override dynamic? OverrideMethod()
12        {
13            var defaultCategory = meta.Target.Project.LoggingOptions().DefaultCategory;
14
15            Console.WriteLine( $"{this.Category ?? defaultCategory}: Executing {meta.Target.Method}." );
16
17            return meta.Proceed();
18        }
19    }
20}
1using Metalama.Framework.Fabrics;
2using System.Linq;
3
4namespace Doc.AspectConfiguration
5{
6    // The project fabric configures the project at compile time.
7    public class Fabric : ProjectFabric
8    {
9        public override void AmendProject( IProjectAmender amender )
10        {
11            amender.Project.LoggingOptions().DefaultCategory = "MyCategory";
12
13            // Adds the aspect to all members.
14            amender.Outbound
15                .SelectMany( c => c.Types.SelectMany( t => t.Methods ) )
16                .AddAspectIfEligible<LogAttribute>();
17        }
18    }
19
20}
Source Code
1namespace Doc.AspectConfiguration


2{
3    // Some target code.
4    public class SomeClass
5    {
6        public void SomeMethod() { }


7    }


8}
Transformed Code
1using System;
2
3namespace Doc.AspectConfiguration
4{
5    // Some target code.
6    public class SomeClass
7    {
8        public void SomeMethod()
9        {
10            Console.WriteLine("MyCategory: Executing SomeClass.SomeMethod().");
11            return;
12        }
13    }
14}
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Project;
3using System;
4
5namespace Doc.AspectConfiguration
6{
7    // Options for the [Log] aspects.
8    public class LoggingOptions : ProjectExtension
9    {
10        private string _defaultCategory = "Default";
11
12        public override void Initialize( IProject project, bool isReadOnly )
13        {
14            base.Initialize( project, isReadOnly );
15
16            // Optionally, we can initialize the configuration object from properties passed from MSBuild.
17            if ( project.TryGetProperty( "DefaultLogProperty", out var propertyValue ) )
18            {
19                this._defaultCategory = propertyValue;
20            }
21        }
22
23        public string DefaultCategory
24        {
25            get => this._defaultCategory;
26
27            set
28            {
29                if ( this.IsReadOnly )
30                {
31                    throw new InvalidOperationException();
32                }
33
34                this._defaultCategory = value;
35            }
36        }
37    }
38
39    // For convenience, an extension method to access the options.
40    [CompileTime]
41    public static class LoggingProjectExtensions
42    {
43        public static LoggingOptions LoggingOptions( this IProject project ) => project.Extension<LoggingOptions>();
44    }
45}