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:
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>
Create a second file named
buildTransitive/YourProject.props
.<Project> <Import Project="../build/YourProject.props"/> </Project>
Include both
YourProject.props
in your project and mark it for inclusion in your NuGet package, respectively. Yourcsproj
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:
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.
From the command line, using the
-p:PropertyName=PropertyValue
command-line argument todotnet
ormsbuild
.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
6public 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}
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 System;
5using System.Diagnostics;
6
7namespace Doc.AspectConfiguration_ProjectDefault;
8
9// Options for the [Log] aspects.
10public class LoggingOptions : IHierarchicalOptions<IMethod>, IHierarchicalOptions<INamedType>,
11 IHierarchicalOptions<INamespace>, IHierarchicalOptions<ICompilation>
12{
13 private static readonly DiagnosticDefinition<string> _invalidLogLevelWarning = new(
14 "MY001",
15 Severity.Warning,
16 "The 'DefaultLogLevel' MSBuild property was set to the invalid value '{0}' and has been ignored." );
17
18 public string? Category { get; init; }
19
20 public TraceLevel? Level { get; init; }
21
22 object IIncrementalObject.ApplyChanges( object changes, in ApplyChangesContext context )
23 {
24 var other = (LoggingOptions) changes;
25
26 return new LoggingOptions
27 {
28 Category = other.Category ?? this.Category, Level = other.Level ?? this.Level
29 };
30 }
31
32 IHierarchicalOptions IHierarchicalOptions.GetDefaultOptions(
33 OptionsInitializationContext context )
34 {
35 context.Project.TryGetProperty( "DefaultLogCategory", out var defaultCategory );
36
37 if ( string.IsNullOrWhiteSpace( defaultCategory ) )
38 {
39 defaultCategory = "Trace";
40 }
41 else
42 {
43 defaultCategory = defaultCategory.Trim();
44 }
45
46 TraceLevel defaultLogLevel;
47
48 context.Project.TryGetProperty( "DefaultLogLevel", out var defaultLogLevelString );
49
50 if ( string.IsNullOrWhiteSpace( defaultLogLevelString ) )
51 {
52 defaultLogLevel = TraceLevel.Verbose;
53 }
54 else
55 {
56 if ( !Enum.TryParse( defaultLogLevelString.Trim(), out defaultLogLevel ) )
57 {
58 context.Diagnostics.Report(
59 _invalidLogLevelWarning.WithArguments( defaultLogLevelString ) );
60
61 defaultLogLevel = TraceLevel.Verbose;
62 }
63 }
64
65 return new LoggingOptions { Category = defaultCategory, Level = defaultLogLevel };
66 }
67}