Metalama's true strength lies not in its pre-made features but in its ability to let you create custom rules for validating the codebase against your architecture.
In this article, we will demonstrate how to extend the Metalama.Extensions.Architecture package. This package is open source. For a better understanding of the instructions provided in this article, you can study its source code.
Extending usage verification with custom predicates
Before creating rules from scratch, it's worth noting that some of the 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, which determines from which scope the declaration can or cannot be referenced. Examples of predicates are CurrentNamespace, NamespaceOf of 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. 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
true
if the predicate matches the given context (i.e., 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.Framework.Aspects;
4using Metalama.Framework.Code;
5using Metalama.Framework.Fabrics;
6using Metalama.Framework.Validation;
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.Framework.Aspects;
4using Metalama.Framework.Code;
5using Metalama.Framework.Fabrics;
6using Metalama.Framework.Validation;
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
53
54
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 (it is not necessary 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 want to create your own validation rule, the first decision is whether it will be available as a custom attribute, as a compile-time method invoked from a fabric, or as both a custom attribute and a compile-time method. 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 of 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 of the rule or the references to the target declaration, i.e., how the target declaration is being used. For instance, if you want to forbid an interface to be implemented by a struct
, you must verify references. However, if you want to verify that no method has more than five parameters, you need to validate the type itself and 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. We will detail this below.
Creating a custom attribute rule
If it is exposed as a custom attribute, it must be implemented as an aspect, but an aspect that does not transform the code, i.e., does not 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. You have several options:
- If you need to validate the target declaration itself, or its members, you can inspect the code model under
builder.Target
and report diagnostics usingbuilder.Diagnostics.Report
. - If you need to validate the references to the target declarations, see Validating code from an aspect.
- If you need to validate the target declaration itself, or its members, you can 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
static
class 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 static
extension method with athis
parameter of type IValidatorReceiver<TDeclaration> whereT
is 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.
- From here, you have several options:
- If you already know, based on the Select, SelectMany and Where methods, that the declaration violates the rule, you can immediately report a warning or error using the ReportDiagnostic method.
- To validate references (i.e. dependencies), use ValidateInboundReferences.
- If your validation logic depends on which aspects were applied, or how aspects transformed the code, call AfterAllAspects() and then register a validator using Validate.
To learn more, it's best to study the source code of the Metalama.Extensions.Architecture
namespace.