uid | level | summary |
---|---|---|
eligibility |
300 |
The document explains how to define eligibility for aspects in code, ensuring predictable behavior, standard error messages, and relevant IDE suggestions. It provides examples and discusses benefits, custom eligibility conditions, eligibility builders, and when to emit custom errors. |
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.
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.
To define the eligibility of your aspect, implement or override the xref:Metalama.Framework.Eligibility.IEligible`1.BuildEligibility* method of the aspect. Use the builder
parameter, which is of type xref:Metalama.Framework.Eligibility.IEligibilityBuilder`1, to specify the requirements of your aspect. For instance, use xref:Metalama.Framework.Eligibility.EligibilityExtensions.MustNotBeAbstract*?text=builder.MustNotBeAbstract() to require a non-abstract method.
Note
Your implementation of xref:Metalama.Framework.Eligibility.IEligible`1.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.
In the following example, we restrict the eligibility of a logging aspect to non-static methods.
[!metalama-test ~/code/Metalama.Documentation.SampleCode.AspectFramework/Eligibility.cs name="Eligibility"]
The Must*
methods of the xref:Metalama.Framework.Eligibility.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 xref:Metalama.Framework.Eligibility.EligibilityExtensions.DeclaringType*, xref:Metalama.Framework.Eligibility.EligibilityExtensions.ReturnType*, or xref:Metalama.Framework.Eligibility.EligibilityExtensions.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.
In the following example, we require the aspect to be used with static types only.
[!metalama-test ~/code/Metalama.Documentation.SampleCode.AspectFramework/Eligibility_DeclaringType.cs name="Eligibility"]
Notice how informative the error message in the target code is: the use of xref:Metalama.Framework.Eligibility.EligibilityExtensions.DeclaringType* informs Metalama to use this information in the error message for the user's benefit.
The xref:Metalama.Framework.Eligibility.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 xref:Metalama.Framework.Eligibility.EligibilityExtensions.MustSatisfy* and define your condition using the xref:Metalama.Framework.Code namespace. You must supply two lambda expressions:
-
The first lambda is a predicate that should return
true
if the proposed declaration is a valid target. -
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, uset => $"{t} must not be a record type"
.
The following example demonstrates the use of xref:Metalama.Framework.Eligibility.EligibilityExtensions.MustSatisfy* to mark record types as ineligible.
[!metalama-test ~/code/Metalama.Documentation.SampleCode.AspectFramework/Eligibility_Custom.cs name="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 xref:Metalama.Framework.Eligibility.EligibilityExtensions.MustSatisfy* to create a custom condition. A more straightforward approach is to use the xref:Metalama.Framework.Eligibility.EligibilityExtensions.If* method.
To convert an xref:Metalama.Framework.Eligibility.IEligibilityBuilder`1 of one declaration type to an xref:Metalama.Framework.Eligibility.IEligibilityBuilder`1 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 xref:Metalama.Framework.Eligibility.EligibilityExtensions.If* followed by a Convert().To<T>()
.
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
orstring
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 xref:diagnostics.
The following example expands the previous one, reporting custom errors when the target class does not define a field logger
of type TextWriter
.
[!metalama-test ~/code/Metalama.Documentation.SampleCode.AspectFramework/EligibilityAndValidation.cs name="Eligibility and Validation"]