Customizing the change merging process
By default, options are inherited along several axes (ApplyChangesAxis). When multiple options apply to the same declaration, they are merged using the ApplyChanges method. This method takes in an ApplyChangesContext object, which exposes the ApplyChangesAxis over which options are being merged.
To ensure a consistent user experience across different aspect libraries, aspect authors are advised to customize options inheritance only when there is a compelling reason to do so.
Merging of options defined for a specific declaration
If multiple sources (such as fabrics, attributes, aspects) set the options for the same declaration, these options are first merged at the level of the node where they have been defined, irrespective of any inheritance. The sources are evaluated in the following order:
- Options provided by custom attributes implementing IHierarchicalOptionsProvider, except aspects. When overriding options provided by competing custom attributes, Metalama uses the axis named SameDeclaration.
- Options set by fabrics using amender.Outgoing.SetOptions. Metalama continues to use the SameDeclaration axis when applying options originating from SetOptions.
- Options set by aspects using aspectBuilder.Outgoing.SetOptions. The SameDeclaration axis is still used.
- Options provided by aspects implementing IHierarchicalOptionsProvider. In this case, the Aspect axis is used.
Merging of inherited options
Once all options have been merged at the level of a specific declaration, inheritance rules apply. Options are applied in the following order of priority, with the first items on the list being overridden by the subsequent items:
- Default options provided by the GetDefaultOptions method for the current project.
- Namespace-level options, from the root to the leaves. When merging namespace-level options with each other and with default options, the axis named ContainingDeclaration is used.
- Options of the base type or overridden member. When base options override namespace options, the BaseDeclaration axis is used. Note that when considering options of the base type or member, fully merged options are considered. This means that namespace-level options of the base type take precedence over the namespace-level options of the current type. In the case of cross-project type inheritance, the default options of the base project take precedence over the default options of the current project.
- Options of the enclosing declaration, recursively, up to but not including the level of namespaces. When options of the declaring type override options inherited from the base type or member, the ContainingDeclaration axis is used.
- Options defined on the target declaration itself. The name of this axis is TargetDeclaration.
While these details may initially seem complex, they ensure an intuitive use of options.
Disabling inheritance axes
If you don't want your options to be inherited along one of the above axes, you can annotate your option class with the [HierarchicalOptions] attribute and set one of these properties to
false: InheritedByDerivedTypes, InheritedByMembers, InheritedByNestedTypes, or InheritedByOverridingMembers.
Hand-tuning the merging process
If you need to further customize the merging process, you can make your ApplyChanges implementation depend on the ApplyChangesContext parameter. This allows you to handle each option property differently according to the context.
Please note that this should be considered an extreme case and we currently do not see a valid use case at the time of writing this documentation.