Open sandboxFocusImprove this doc

Defining the eligibility of aspects

Most aspects are designed and implemented for specific kinds of target declarations. For instance, you may decide that your caching aspect will not support void methods or methods with out or ref parameters. As the author of the aspect, it is essential to ensure that users of your aspect apply it only to the declarations that you expect. Otherwise, the aspect may cause build errors with confusing messages or even incorrect run-time behavior.

Benefits

Defining the eligibility of an aspect provides the following benefits:

  • Predictable behavior: Applying an aspect to a declaration for which the aspect was not designed or tested can be a very confusing experience for your users due to error messages they may not understand. As the author of the aspect, it is your responsibility to ensure that using your aspect is easy and predictable.
  • Standard error messages: All eligibility error messages are standard, making them easier to understand for aspect users.
  • Relevant suggestions in the IDE: The IDE will only propose code actions in the refactoring menu for eligible declarations.

Defining eligibility

To define the eligibility of your aspect, implement or override the BuildEligibility method of the aspect. Use the builder parameter, which is of type IEligibilityBuilder<T>, to specify the requirements of your aspect. For instance, use builder.MustNotBeAbstract() to require a non-abstract method.

Note

Your implementation of BuildEligibility must not reference any instance member of the class. This method is called on an instance obtained using FormatterServices.GetUninitializedObject, i.e., without invoking the class constructor.

Example: allowing instance methods only

In the following example, we restrict the eligibility of a logging aspect to non-static methods.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Eligibility;
4
5namespace Doc.Eligibility;
6
7internal class LogAttribute : OverrideMethodAspect
8{
9    public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
10    {
11        base.BuildEligibility( builder );
12
13        // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
14        builder.MustNotBeStatic();
15    }
16
17    public override dynamic? OverrideMethod()
18    {
19        meta.This._logger.WriteLine( $"Executing {meta.Target.Method}" );
20
21        return meta.Proceed();
22    }
23}
1using System;
2using System.IO;
3
4namespace Doc.Eligibility;
5
6internal class SomeClass
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void InstanceMethod() { }
12
    Error LAMA0037: The aspect 'Log' cannot be applied to the method 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must not be static.

13    [Log]
14    private static void StaticMethod() { }
15}

Validating the declaring type, parameter type, or return type

The Must* methods of the EligibilityExtensions class apply to the direct aspect of the aspect. If you want to validate something else, such as the declaring type of the member or the method return type, use methods like DeclaringType, ReturnType, or Parameter before calling the Must* method.

The benefit of using these methods is that the error message is more informative when the user attempts to add the aspect to an ineligible condition.

Example: allowing static types only

In the following example, we require the aspect to be used with static types only.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Eligibility;
4using System;
5
6namespace Doc.Eligibility_DeclaringType;
7
8internal class StaticLogAttribute : OverrideMethodAspect
9{
10    public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
11    {
12        base.BuildEligibility( builder );
13
14        // The aspect must only be used on static classes.
15        builder.DeclaringType().MustBeStatic();
16    }
17
18    public override dynamic? OverrideMethod()
19    {
20        Console.WriteLine( $"Executing {meta.Target.Method}" );
21
22        return meta.Proceed();
23    }
24}
1namespace Doc.Eligibility_DeclaringType;
2
3internal record SomeClass( int Foo )
4{
    Error LAMA0037: The aspect 'StaticLog' cannot be applied to the method 'SomeClass.SomeMethod()' because the declaring type 'SomeClass' must be static.

5    [StaticLog]
6    private void SomeMethod() { }
7}

Notice how informative the error message in the target code is: the use of DeclaringType informs Metalama to use this information in the error message for the user's benefit.

Defining custom eligibility conditions

The EligibilityExtensions class defines the most common eligibility conditions. However, you will often need to express conditions for which no ready-made method exists. In this situation, you can add a custom eligibility condition by calling MustSatisfy and define your condition using the Metalama.Framework.Code namespace. You must supply two lambda expressions:

  1. The first lambda is a predicate that should return true if the proposed declaration is a valid target.

  2. The second lambda is only evaluated when the proposed declaration is not a valid target and should return a user-readable string that explains why the declaration is not eligible.

    • This lambda must return a formattable string. Attempting to format the string yourself is not recommended as we are using a custom formatter.
    • To include the description of the ineligible declaration in the formattable string, just use the raw input argument. It will be properly formatted.
    • We adopted the convention that this message says what the declaration must be in order to be eligible instead of saying what it must not be because it generally combines better when there are several eligibility conditions.

    For instance, if your aspect does not support record types, use t => $"{t} must not be a record type".

Example: forbidding record types

The following example demonstrates the use of MustSatisfy to mark record types as ineligible.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Eligibility;
4
5namespace Doc.Eligibility_Custom;
6
7internal class LogAttribute : OverrideMethodAspect
8{
9    public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
10    {
11        base.BuildEligibility( builder );
12
13        // The aspect must not be offered on record classes.
14        builder
15            .DeclaringType()
16            .MustSatisfy(
17                t => t.TypeKind is not (TypeKind.RecordClass or TypeKind.RecordStruct),
18                t => $"{t} must not be a record type" );
19    }
20
21    public override dynamic? OverrideMethod()
22    {
23        meta.This._logger.WriteLine( $"Executing {meta.Target.Method}" );
24
25        return meta.Proceed();
26    }
27}
1namespace Doc.Eligibility_Custom;
2
3internal record SomeClass( int Foo )
4{
    Error LAMA0037: The aspect 'Log' cannot be applied to the method 'SomeClass.SomeMethod()' because the declaring type 'SomeClass' must not be a record type.

5    [Log]
6    private void SomeMethod() { }
7}

Adding "if" clauses to eligibility

Sometimes the eligibility of aspects depends on a condition. For instance, your aspect may be eligible for all instance methods but only void static methods. One approach is to use MustSatisfy to create a custom condition. A more straightforward approach is to use the If method.

Converting eligibility builders

To convert an IEligibilityBuilder<T> of one declaration type to an IEligibilityBuilder<T> for another type, use the builder.Convert().To<T>() method. This will implicitly add an eligibility condition that the declaration is a T.

Alternatively, when you don't want this implicit condition, you can use builder.Convert().When<T>(). This is equivalent to using an If followed by a Convert().To<T>().

When to emit custom errors instead?

It may be tempting to add an eligibility condition for every requirement of your aspect instead of emitting a custom error message. However, this may be confusing for the user.

As a rule of thumb, you should use eligibility to define those declarations for which it makes sense to apply the aspect or not and use error messages when the aspect makes sense on the declaration, but some contingency may prevent the aspect from being used. This is where you should report errors.

For instance:

  • Adding a caching aspect to a void method does not make sense and should be addressed with eligibility. However, the fact that your aspect does not support methods returning a collection is a limitation caused by your particular implementation and should be reported using a custom error.

  • Adding a dependency injection aspect to an int or string field does not make sense, and this condition should be expressed using the eligibility API. However, the fact that your implementation of the aspect requires the field to be non-read-only is a contingency and should be reported as an error.

For details about reporting errors, see Reporting and suppressing diagnostics.

Example: combining eligibility with diagnostics

The following example expands the previous one, reporting custom errors when the target class does not define a field logger of type TextWriter.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using Metalama.Framework.Eligibility;
5using System.IO;
6using System.Linq;
7
8namespace Doc.EligibilityAndValidation;
9
10internal class LogAttribute : OverrideMethodAspect
11{
12    private static readonly DiagnosticDefinition<INamedType> _error1 = new(
13        "MY001",
14        Severity.Error,
15        "The type '{0}' must have a field named '_logger'." );
16
17    private static readonly DiagnosticDefinition<IField> _error2 = new(
18        "MY002",
19        Severity.Error,
20        "The type of the field '{0}' must be 'TextWriter'." );
21
22    public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
23    {
24        base.BuildEligibility( builder );
25
26        // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
27        builder.MustNotBeStatic();
28    }
29
30    public override void BuildAspect( IAspectBuilder<IMethod> builder )
31    {
32        base.BuildAspect( builder );
33
34        // Validate that the target file has a field named 'logger' of type TextWriter.
35        var declaringType = builder.Target.DeclaringType;
36        var loggerField = declaringType.Fields.OfName( "_logger" ).SingleOrDefault();
37
38        if ( loggerField == null )
39        {
40            builder.Diagnostics.Report( _error1.WithArguments( declaringType ), declaringType );
41            builder.SkipAspect();
42        }
43        else if ( !loggerField.Type.Is( typeof(TextWriter) ) )
44        {
45            builder.Diagnostics.Report( _error2.WithArguments( loggerField ), loggerField );
46            builder.SkipAspect();
47        }
48    }
49
50    public override dynamic? OverrideMethod()
51    {
52        meta.This.logger.WriteLine( $"Executing {meta.Target.Method}" );
53
54        return meta.Proceed();
55    }
56}
1namespace Doc.EligibilityAndValidation;
2
3internal class SomeClass
4{
    Error MY002: The type of the field 'SomeClass._logger' must be 'TextWriter'.

5    private object? _logger;
6
7    [Log]
8    private void InstanceMethod() { }
9
    Error LAMA0037: The aspect 'Log' cannot be applied to the method 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must not be static.

10    [Log]
11    private static void StaticMethod() { }
12}