MetalamaConceptual documentationCreating aspectsMaking aspects configurableReading MSBuild properties
Open sandboxFocusImprove this doc

Reading MSBuild properties

In addition to or as an alternative to a programmatic configuration API, an aspect can accept configuration by reading MSBuild properties using the IProject.TryGetProperty method.

This strategy enables the aspect to be configured without modifying the source code. This can be useful when you want the aspect to behave differently according to a property supplied from the command line, for example.

Another advantage of accepting MSBuild properties for configuration is that they can be defined in Directory.Build.props and shared among all projects in the repository. For more details, refer to Customize the build by folder in the Visual Studio documentation.

Exposing MSBuild properties

By default, MSBuild properties are not visible to Metalama: you must instruct MSBuild to pass them to the compiler using the CompilerVisibleProperty item.

If you are shipping your project as a NuGet package, we recommend the following approach to consume a configuration property:

  1. Create a file named build/YourProject.props.

    Warning

    The file name must exactly match the name of your package.

    <Project>
        <ItemGroup>
            <CompilerVisibleProperty Include="YourProperty" />
        </ItemGroup>
    </Project>
    
  2. Create a second file named buildTransitive/YourProject.props.

    <Project>
        <Import Project="../build/YourProject.props"/>
    </Project>
    
  3. Include both YourProject.props in your project and mark it for inclusion in your NuGet package, respectively. Your csproj file should look like this:

    <Project Sdk="Microsoft.NET.Sdk">
        <!-- ... -->
        <ItemGroup>
            <None Include="build/*">
                <Pack>true</Pack>
                <PackagePath></PackagePath>
            </None>
            <None Include="buildTransitive/*">
                <Pack>true</Pack>
                <PackagePath></PackagePath>
            </None>
        </ItemGroup>
        <!-- ... -->
    </Project>
    

This approach will make sure that YourProject.props is automatically included in any project that references your project using a PackageReference.

However, this will not work for projects referencing your project using a PackageReference. In this case, you need to manually import the YourProject.props file using the following code:

<Import Project="../YourProject/build/YourProject.props"/>

Setting MSBuild properties

To configure the aspect, users should set this property using one of the following approaches:

  1. By modifying 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 can cause your aspect to receive an incorrect value.

  2. From the command line, using the -p:PropertyName=PropertyValue command-line argument to dotnet or msbuild.

  3. By setting an environment variable. See the MSBuild documentation for details.

Reading MSBuild properties from an aspect or fabric

To read an MSBuild property, use the IProject.TryGetProperty method. The IProject object is available almost everywhere. If you have an IDeclaration, use declaration.Compilation.Project.

Example: reading MSBuild properties from an aspect

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

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}

Combining MSBuild properties with the options API

Whenever your aspect library relies on both MSBuild properties and a configuration API, it is recommended to integrate the MSBuild properties with your option class instead of reading the properties directly from the aspect classes.

Instead, properties should be read from the GetDefaultOptions method of your option class. This method receives a parameter of type OptionsInitializationContext, which exposes the IProject interface, and allows you to read the properties. The object also lets you report errors or warnings if the properties have an invalid value. Thanks to this approach, you can make the default options dependent on MSBuild properties.

Examples: building default options from MSBuild properties

In the following example, the options class implements the GetDefaultOptions to read default values from the MSBuild properties. It reports a diagnostic if their value is incorrect.

1using Metalama.Framework.Code;
2using Metalama.Framework.Diagnostics;
3using Metalama.Framework.Options;
4using Metalama.Framework.Project;
5using System;
6using System.Diagnostics;
7
8namespace Doc.AspectConfiguration_ProjectDefault
9{
10    // Options for the [Log] aspects.
11    public class LoggingOptions : IHierarchicalOptions<IMethod>, IHierarchicalOptions<INamedType>,
12                                  IHierarchicalOptions<INamespace>, IHierarchicalOptions<ICompilation>
13    {
14        private static readonly DiagnosticDefinition<string> _invalidLogLevelWarning = new(
15            "MY001",
16            Severity.Warning,
17            "The 'DefaultLogLevel' MSBuild property was set to the invalid value '{0}' and has been ignored." );
18
19        public string? Category { get; init; }
20
21        public TraceLevel? Level { get; init; }
22
23        object IIncrementalObject.ApplyChanges( object changes, in ApplyChangesContext context )
24        {
25            var other = (LoggingOptions) changes;
26
27            return new LoggingOptions { Category = other.Category ?? this.Category, Level = other.Level ?? this.Level };
28        }
29
30        IHierarchicalOptions IHierarchicalOptions.GetDefaultOptions( OptionsInitializationContext context )
31        {
32            context.Project.TryGetProperty( "DefaultLogCategory", out var defaultCategory );
33
34            if ( string.IsNullOrWhiteSpace( defaultCategory ) )
35            {
36                defaultCategory = "Trace";
37            }
38            else
39            {
40                defaultCategory = defaultCategory.Trim();
41            }
42
43            TraceLevel defaultLogLevel;
44
45            context.Project.TryGetProperty( "DefaultLogLevel", out var defaultLogLevelString );
46
47            if ( string.IsNullOrWhiteSpace( defaultLogLevelString ) )
48            {
49                defaultLogLevel = TraceLevel.Verbose;
50            }
51            else
52            {
53                if ( !Enum.TryParse( defaultLogLevelString.Trim(), out defaultLogLevel ) )
54                {
55                    context.Diagnostics.Report( _invalidLogLevelWarning.WithArguments( defaultLogLevelString ) );
56                    defaultLogLevel = TraceLevel.Verbose;
57                }
58            }
59
60            return new LoggingOptions { Category = defaultCategory, Level = defaultLogLevel };
61        }
62    }
63}