Metalama's true strength lies not in its pre-made features but in letting you create custom rules for validating your codebase against your architecture.
This article demonstrates how to extend the Metalama.Extensions.Architecture package.
Extending usage verification with custom predicates
Before creating rules from scratch, note that some existing rules can be extended. In Verifying usage of a class, member, or namespace, you learned how to use methods like CanOnlyBeUsedFrom or CannotBeUsedFrom. These methods require a predicate parameter that determines from which scope the declaration can or can't be referenced. Examples of predicates are CurrentNamespace and NamespaceOf from the ReferencePredicateExtensions class. The role of predicates is to determine whether a given code reference should report a warning.
To implement a new predicate, follow these steps:
Create a new class and derive it from ReferencePredicate. For convenience, you can also derive from ReferenceEndPredicate, which simplifies predicates that rely on a single reference end (referencing or referenced). We recommend making this class
internal.Add fields for all predicate parameters, and initialize these fields from the constructor.
Note
Predicate objects are serialized. Therefore, all fields must be serializable. Notably, objects of IDeclaration type are not serializable. To serialize a declaration, call the ToRef method and store the returned IRef<T>. For details, see Serialization of aspects and other compile-time classes.
Implement the IsMatch method. This method receives a ReferenceValidationContext. It must return
trueif the predicate matches the given context (the code reference), otherwisefalse.Create an extension method for the ReferencePredicateBuilder type and return a new instance of your predicate class.
Example: restricting usage based on calling method name
In the following example, we create a custom predicate, MethodNameEndsWith, which verifies that the code reference occurs within a method whose name ends with a given suffix.
1using Metalama.Extensions.Architecture;
2using Metalama.Extensions.Architecture.Predicates;
3using Metalama.Extensions.Validation;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Fabrics;
7using System;
8
9namespace Doc.Architecture.Fabric_CustomPredicate;
10
11// This class is the actual implementation of the predicate.
12internal class MethodNamePredicate : ReferenceEndPredicate
13{
14 private readonly string _suffix;
15
16 public MethodNamePredicate( ReferencePredicateBuilder builder, string suffix ) : base( builder )
17 {
18 this._suffix = suffix;
19 }
20
21 protected override ReferenceGranularity GetGranularity() => ReferenceGranularity.Member;
22
23 public override bool IsMatch( ReferenceEnd referenceEnd )
24 => referenceEnd.Member is IMethod method && method.Name.EndsWith(
25 this._suffix,
26 StringComparison.Ordinal );
27}
28
29// This class exposes the predicate as an extension method. It is your public API.
30[CompileTime]
31public static class Extensions
32{
33 public static ReferencePredicate MethodNameEndsWith(
34 this ReferencePredicateBuilder builder,
35 string suffix )
36 => new MethodNamePredicate( builder, suffix );
37}
38
39// Here is how your new predicate can be used.
40internal class Fabric : ProjectFabric
41{
42 public override void AmendProject( IProjectAmender amender )
43 {
44 amender.SelectReflectionType( typeof(CofeeMachine) )
45 .CanOnlyBeUsedFrom( r => r.MethodNameEndsWith( "Politely" ) );
46 }
47}
48
49// This is the class whose access are validated.
50internal static class CofeeMachine
51{
52 public static void TurnOn() { }
53}
54
55internal class Bar
56{
57 public static void OrderCoffee()
58 {
59 // Forbidden because the method name does not end with Politely.
Warning LAMA0905: The 'CofeeMachine' type cannot be referenced by the 'Bar.OrderCoffee()' method.
60 CofeeMachine.TurnOn();
61 }
62
63 public static void OrderCoffeePolitely()
64 {
65 // Allowed.
66 CofeeMachine.TurnOn();
67 }
68}
1using Metalama.Extensions.Architecture;
2using Metalama.Extensions.Architecture.Predicates;
3using Metalama.Extensions.Validation;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Fabrics;
7using System;
8
9namespace Doc.Architecture.Fabric_CustomPredicate;
10
11// This class is the actual implementation of the predicate.
12
13#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
14
15
16internal class MethodNamePredicate : ReferenceEndPredicate
17{
18 private readonly string _suffix;
19
20 public MethodNamePredicate(ReferencePredicateBuilder builder, string suffix) : base(builder)
21 {
22 this._suffix = suffix;
23 }
24
25 protected override ReferenceGranularity GetGranularity() => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
26
27
28 public override bool IsMatch(ReferenceEnd referenceEnd) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
29}
30
31#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
32
33
34
35// This class exposes the predicate as an extension method. It is your public API.
36
37#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
38
39
40[CompileTime]
41public static class Extensions
42{
43 public static ReferencePredicate MethodNameEndsWith(this ReferencePredicateBuilder builder, string suffix) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
44}
45
46#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
47
48
49
50// Here is how your new predicate can be used.
51
52#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
Warning LAMA0905: The 'CofeeMachine' type cannot be referenced by the 'Bar.OrderCoffee()' method.
53
54
Warning LAMA0905: The 'CofeeMachine' type cannot be referenced by the 'Bar.OrderCoffee()' method.
55internal class Fabric : ProjectFabric
56{
57 public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
58}
59
60#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
61
62
63
64// This is the class whose access are validated.
65internal static class CofeeMachine
66{
67 public static void TurnOn() { }
68}
69
70internal class Bar
71{
72 public static void OrderCoffee()
73 {
74 // Forbidden because the method name does not end with Politely.
75 CofeeMachine.TurnOn();
76 }
77
78 public static void OrderCoffeePolitely()
79 {
80 // Allowed.
81 CofeeMachine.TurnOn();
82 }
83}
Creating new verification rules
Before you build custom validation rules, you should have a basic understanding of the following topics:
- Understanding the aspect framework design (you don't need to learn about advising the code)
- Reporting and suppressing diagnostics
- Defining the eligibility of aspects
- Applying aspects to derived types
- Fabrics
Designing the rule
When you create your own validation rule, the first decision is whether it'll be available as a custom attribute, as a compile-time method invoked from a fabric, or both. As a rule of thumb, use attributes when rules need to be applied one by one by the developer and use fabrics when rules apply to a large set of declarations according to a code query that can be expressed programmatically. Rules that affect namespaces must be implemented as fabric-based rules because adding a custom attribute to a namespace is impossible. For most ready-made rules in the Metalama.Extensions.Architecture namespace, we expose both a custom attribute and a compile-time method.
The second question is whether the rule affects the target declaration or the references to the target declaration (how the target declaration is being used). For instance, to forbid an interface from being implemented by a struct, you must verify references. However, to verify that no method has more than five parameters, you need to validate the type itself, not its references.
A third question relates to rules that verify classes: should the rule be inherited from the base type to derived types? For instance, if you want all implementations of the IFactory interface to have a parameterless constructor, you may implement it as an inheritable aspect. However, with inheritable rules, the design process may be more complex.
Creating a custom attribute rule
If it's exposed as a custom attribute, it must be implemented as an aspect, but an aspect that doesn't transform the code (doesn't provide any advice).
Follow these steps:
Create a new class from one of the following classes: ConstructorAspect, EventAspect, FieldAspect, FieldOrPropertyAspect, MethodAspect, ParameterAspect, PropertyAspect, TypeAspect, TypeParameterAspect
All of these classes derive from the Attribute system class.
If your rule must be inherited, add the [Inheritable] attribute to the class. See Applying aspects to derived types for details.
For each error or warning you plan to report, add a static field of type DiagnosticDefinition to your aspect class, as described in Reporting and suppressing diagnostics.
Implement the BuildAspect method:
- To validate the target declaration itself or its members, inspect the code model under
builder.Targetand report diagnostics usingbuilder.Diagnostics.Report. - To validate the references to the target declarations, see Validating code from an aspect.
- To validate the target declaration itself or its members, inspect the code model under
1namespace Doc.Architecture.RequireDefaultConstructorAspect;
2
3// Apply the aspect to the base class. It will be inherited to all derived classes.
4[RequireDefaultConstructor]
5public class BaseClass { }
6
7// This class has an implicit default constructor.
8public class ValidClass1 : BaseClass { }
9
10// This class has an explicit default constructor.
11public class ValidClass2 : BaseClass
12{
13 public ValidClass2() { }
14}
15
16// This class has no default constructor.
Warning MY001: The type 'InvalidClass' must have a public default constructor.
17public class InvalidClass : BaseClass
18{
19 public InvalidClass( int x ) { }
20}
Creating a programmatic rule
Follow this procedure:
Create a
staticclass containing your extension methods. Name it, for instance,ArchitectureExtensions.Add the [CompileTime] custom attribute to the class.
For each error or warning you plan to report, add a static field of type DiagnosticDefinition to your fabric class, as described in Reporting and suppressing diagnostics.
Create a
public staticextension method with athisparameter of type IQuery<TDeclaration> whereTis the type of declarations you want to validate. Name it for instanceverifier.If you need to apply the rule to contained declarations, select them using the Select, SelectMany, and Where methods.
Choose from the following options:
- If you already know, based on the Select, SelectMany, and Where methods, that the declaration violates the rule, immediately report a warning or error using the ReportDiagnostic method.
- To validate references (dependencies), use ValidateInboundReferences.
- To validate the declaration itself, use Validate.