Project "Caravela" 0.3 / / Caravela Documentation / Conceptual Documentation / Creating Aspects / Defining the Eligibility of Aspects

Defining the Eligibility of Aspects

Why to define eligibility?

Most of the 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 an out or ref parameter. It is important, as the author of the aspect, to make sure that the user of your aspect applies it only to the declarations that you expect. Otherwise, at best, the aspect will cause build errors and confuse the user. At worse, the run-time behavior of your aspect will be incorrect.

By defining the eligibility, you ensure that:

  • the IDE or the compiler will report a nice error message when the user tries to add the aspect to an unsupported declaration, and that
  • the IDE will only propose code action 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, of type IEligibilityBuilder<T>, to specify the requirements of your aspect. For instance, use builder.MustBeNonAbstract() to require a non-abstract method.

A number of predefined eligibility conditions are implemented by the EligibilityExtensions static class. You can add a custom eligibility condition by calling MustSatisfy and providing your own lambda expression. This method also expects the user-readable string that should be included in the error message when the user attempts to add the aspect to an ineligible declaration.

Note

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

Example

using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Eligibility;

namespace Caravela.Documentation.SampleCode.AspectFramework.Eligibility
{
    internal class LogAttribute : OverrideMethodAspect
    {
        public override void BuildEligibility(IEligibilityBuilder<IMethod> builder)
        {
            base.BuildEligibility(builder);

            // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
            builder.MustBeNonStatic();
        }

        public override dynamic? OverrideMethod()
        {
            meta.This.logger.WriteLine($"Executing {meta.Target.Method}");

            return meta.Proceed();
        }
    }
}
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Eligibility;
using System;
using System.IO;

namespace Caravela.Documentation.SampleCode.AspectFramework.Eligibility
{
    internal class SomeClass
    {
        TextWriter logger = Console.Out;

        [Log]
        private void InstanceMethod() { }


        [Log]
        private static void StaticMethod() { }
        
    }

}
// Error CR0037 on `Log`: `The aspect 'Log' cannot be applied to 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must be non-static.`

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 for which declarations the aspect makes sense or not, and use error messages when the aspect makes sense on the declaration, but there is some contingency that prevents the aspect from being used.

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

Example 1

Adding a caching 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 an implementation detail and should be reported using a custom error.

Example 2

Adding a dependency injection aspect to an int or string field does not make sense and this condition should therefore 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.

Example

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

using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Diagnostics;
using Caravela.Framework.Eligibility;
using System.IO;
using System.Linq;

namespace Caravela.Documentation.SampleCode.AspectFramework.EligibilityAndValidation
{
    internal class LogAttribute : OverrideMethodAspect
    {
        static DiagnosticDefinition<INamedType> _error1 = new ("MY001", Severity.Error, "The type {0} must have a field named 'logger'.");
        static DiagnosticDefinition<IField> _error2 = new("MY002", Severity.Error, "The type of the field {0} must be 'TextWriter'.");

        public override void BuildEligibility(IEligibilityBuilder<IMethod> builder)
        {
            base.BuildEligibility(builder);

            // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
            builder.MustBeNonStatic();
        }

        public override void BuildAspect(IAspectBuilder<IMethod> builder)
        {
            base.BuildAspect(builder);

            // Validate that the target file has a field named 'logger' of type TextWriter.
            INamedType declaringType = builder.Target.DeclaringType;
            var loggerField = declaringType.Fields.OfName("logger").SingleOrDefault();
            if ( loggerField == null )
            {
                builder.Diagnostics.Report(declaringType, _error1, declaringType);
                builder.SkipAspect();
            }
            else if ( !loggerField.Type.Is(typeof(TextWriter))                )
            {
                builder.Diagnostics.Report(loggerField, _error2, loggerField);
                builder.SkipAspect();
            }
        }

        public override dynamic? OverrideMethod()
        {
            meta.This.logger.WriteLine($"Executing {meta.Target.Method}");

            return meta.Proceed();
        }
    }
}
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Eligibility;
using System;
using System.IO;

namespace Caravela.Documentation.SampleCode.AspectFramework.EligibilityAndValidation
{
    internal class SomeClass
    {
        object? logger;

        [Log]
        private void InstanceMethod() { }


        [Log]
        private static void StaticMethod() { }
        
    }

}
// Error MY002 on `logger`: `The type of the field SomeClass.logger must be 'TextWriter'.`
// Error CR0037 on `Log`: `The aspect 'Log' cannot be applied to 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must be non-static.`