Metalama//Conceptual documentation/Verifying architecture/Extending the validation API
Open sandboxFocus

Creating your own validation rules

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 show how to extend the Metalama.Extensions.Architecture package. This package is open source. To better understand the indications given in this article, you can study its source code.

Extending usage verification with custom predicates

Before we create rules from scratch, it's good to know that some of the existing rules can be extended. In Verifying usage of a class, member, or namespace, you have learned how to use methods like CanOnlyBeUsedFrom or CannotBeUsedFrom. These methods require a predicate parameter, which determine 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 the following steps:

  1. Create a new class and derive it from ReferencePredicate. We recommend making this class internal.
  2. 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>.

  3. 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); otherwise false.

  4. 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 prefix.

Source Code
1using Metalama.Extensions.Architecture.Fabrics;
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    internal class MethodNamePredicate : ReferencePredicate
13    {
14        private readonly string _suffix;
15
16        public MethodNamePredicate( ReferencePredicateBuilder? builder, string suffix ) : base( builder )
17        {
18            this._suffix = suffix;
19        }
20
21        public override bool IsMatch( in ReferenceValidationContext context )
22        {
23            return context.ReferencingDeclaration is IMethod method && method.Name.EndsWith( this._suffix, StringComparison.Ordinal );
24        }
25    }


26
27    // This class exposes the predicate as an extension method. It is your public API.



28    [CompileTime]
29    public static class Extensions
30    {
31        public static ReferencePredicate MethodNameEndsWith( this ReferencePredicateBuilder? builder, string suffix ) => new MethodNamePredicate( builder, suffix );

32    }



33
34    // Here is how your new predicate can be used.



35    internal class Fabric : ProjectFabric
36    {
37        public override void AmendProject( IProjectAmender amender )
38        {
39            amender.Verify().SelectTypes( typeof( CofeeMachine ) ).CanOnlyBeUsedFrom( r => r.MethodNameEndsWith( "Politely" ) );
40        }
41    }



42
43    // This is the class whose access are validated.
44    internal static class CofeeMachine
45    {
46        public static void TurnOn() { }
47    }
48
49    internal class Bar
50    {
51        public static void OrderCoffee()
52        {
53            // Forbidden because the method name does not end with Politely.
            Warning LAMA0905: The 'CofeeMachine' type cannot be referenced by the 'Bar' type.

54            CofeeMachine.TurnOn();
55        }
56
57        public static void OrderCoffeePolitely()
58        {
59            // Allowed.
60            CofeeMachine.TurnOn();
61        }
62    }
63
64
65}
66
Transformed Code
1using Metalama.Extensions.Architecture.Fabrics;
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    internal class MethodNamePredicate : ReferencePredicate
16    {
17        private readonly string _suffix;
18
19        public MethodNamePredicate(ReferencePredicateBuilder? builder, string suffix) : base(builder)
20        {
21            this._suffix = suffix;
22        }
23
24        public override bool IsMatch(in ReferenceValidationContext context) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
25

26    }
27
28#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
29
30
31    // This class exposes the predicate as an extension method. It is your public API.
32
33#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
34
35    [CompileTime]
36    public static class Extensions
37    {
38        public static ReferencePredicate MethodNameEndsWith(this ReferencePredicateBuilder? builder, string suffix) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
39
40    }
41
42#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
43
44
45    // Here is how your new predicate can be used.
46
47#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
48
49    internal class Fabric : ProjectFabric
50    {
51        public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
52


53    }
54
55#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
56
57
58    // This is the class whose access are validated.
59    internal static class CofeeMachine
60    {
61        public static void TurnOn() { }
62    }
63
64    internal class Bar
65    {
66        public static void OrderCoffee()
67        {
68            // Forbidden because the method name does not end with Politely.
69            CofeeMachine.TurnOn();
70        }
71
72        public static void OrderCoffeePolitely()
73        {
74            // Allowed.
75            CofeeMachine.TurnOn();
76        }
77    }
78
79
80}
81

Creating new verification rules

Before you build custom validation rules, you should have a basic understanding of the following topics:

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.

  1. 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.

  2. If your rule must be inherited, add the [Inheritable] attribute to the class. See Applying aspects to derived types for details.

  3. 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.

  4. 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 using builder.Diagnostics.Report.
    • If you need to validate the references to the target declarations, see Validating code from an aspect.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using System.Linq;
5
6namespace Doc.Architecture.RequireDefaultConstructorAspect
7{
8    [Inheritable]
9    public class RequireDefaultConstructorAttribute : TypeAspect
10    {
11        private static DiagnosticDefinition<INamedType> _warning = new( "MY001", Severity.Warning, "The type '{0}' must have a public default constructor." );
12
13        public override void BuildAspect( IAspectBuilder<INamedType> builder )
14        {
15            if ( builder.Target.IsAbstract )
16            {
17                return;
18            }
19
20            var defaultConstructor = builder.Target.Constructors.SingleOrDefault( c => c.Parameters.Count == 0 );
21            if ( defaultConstructor == null || defaultConstructor.Accessibility != Accessibility.Public )
22            {
23                builder.Diagnostics.Report( _warning.WithArguments( builder.Target ) );
24            }
25        }
26    }
27
28}
29
Source Code
1namespace Doc.Architecture.RequireDefaultConstructorAspect
2{
3    // Apply the aspect to the base class. It will be inherited to all derived classes.
4    [RequireDefaultConstructor]
5    public class BaseClass { }
6
7    // This class has an implicit default constructor.
8    public class ValidClass1 : BaseClass { }
9
10    // This class has an explicit default constructor.
11    public 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.

17    public class InvalidClass : BaseClass
18    {
19        public InvalidClass ( int x ) { }
20    }
21}
22
Transformed Code
1namespace Doc.Architecture.RequireDefaultConstructorAspect
2{
3    // Apply the aspect to the base class. It will be inherited to all derived classes.
4    [RequireDefaultConstructor]
5    public class BaseClass { }
6
7    // This class has an implicit default constructor.
8    public class ValidClass1 : BaseClass { }
9
10    // This class has an explicit default constructor.
11    public class ValidClass2 : BaseClass
12    {
13        public ValidClass2() { }
14    }
15
16    // This class has no default constructor.
17    public class InvalidClass : BaseClass
18    {
19        public InvalidClass(int x) { }
20    }
21}
22

Creating a programmatic rule

Follow this procedure

  1. Create a static class containing your extension methods. Name it, for instance, ArchitectureExtensions.

  2. Add the [CompileTime] custom attribute to the class.

  3. 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.

  4. Create a public static extension method with a this parameter and name it verifier.

    If you need to validate ICompilation, INamespace or INamedType, this parameter type should be ITypeSetVerifier<IDeclaration>. Most of the time, you will want to validate the types contained type set of the receiver parameter. To access these types, use the verifier.TypeReceiver property.

    If, however, you need to validate declarations that are not types or type sets the type of the verifier should be ITypeSetVerifier, where T is the base interface that you want to validate. The receiver that allows you to add validators or report diagnostics is available on the verifier.TypeReceiver property.

  5. You can filter the receiver (i.e. either verifier.TypeReceiver or verifier.Receiver), as in System.Linq:

    • Filter the set using the Where method. Typically, you will want to filter out declarations that do not break the rule.
    • Use Select or SelectMany.
  6. When the receiver contains the proper set of declarations, you can take action:

    • Report a diagnostic using <xref: Metalama.Framework.Validation.IValidatorReceiver`1.ReportDiagnostic*>.
    • Register a reference validator using the <xref: Metalama.Framework.Validation.IValidatorReceiver`1.ValidateReferences*> method. Validating from fabrics is very similar to validating from aspects. See Validating code from an aspect for details.
Source Code
1using Metalama.Extensions.Architecture.Fabrics;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Fabrics;
6using System.Linq;
7
8namespace Doc.Architecture.RequireDefaultConstructorFabric
9{
10    // Reusable implementation of the architecture rule.



11    [CompileTime]
12    internal static class ArchitectureExtensions
13    {
14        private static DiagnosticDefinition<INamedType> _warning = new( "MY001", Severity.Warning, "The type '{0}' must have a public default constructor." );
15
16        public static void MustHaveDefaultConstructor( this ITypeSetVerifier<IDeclaration> verifier )
17        {
18            verifier.TypeReceiver
19                .Where( t => !t.IsStatic && t.Constructors.FirstOrDefault( c => c.Parameters.Count == 0 ) is null or { Accessibility: not Accessibility.Public } )
20                .ReportDiagnostic( t => _warning.WithArguments( t ) );
21        }
22    }



23
24

25    internal class Fabric : ProjectFabric
26    {
27        public override void AmendProject( IProjectAmender amender )
28        {
29            // Using the reusable MustHaveDefaultConstructor rule.
30            // Note that we only apply the rule to public types. 
31            amender.Verify().Types().Where( t => t.Accessibility == Accessibility.Public ).MustHaveDefaultConstructor();
32        }
33    }


34
35    // This class has an implicit default constructor.
36    public class ValidClass1 { }
37
38    // This class has an explicit default constructor.
39    public class ValidClass2
40    {
41        public ValidClass2() { }
42    }
43
44    // This class does not havr any default constructor.
    Warning MY001: The type 'InvalidClass' must have a public default constructor.

45    public class InvalidClass
46    {
47        public InvalidClass( int x ) { }
48    }
49}
50
Transformed Code
1using Metalama.Extensions.Architecture.Fabrics;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Fabrics;
6using System.Linq;
7
8namespace Doc.Architecture.RequireDefaultConstructorFabric
9{
10    // Reusable implementation of the architecture rule.
11
12#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
13
14    [CompileTime]
15    internal static class ArchitectureExtensions
16    {
17        private static DiagnosticDefinition<INamedType> _warning = new("MY001", Severity.Warning, "The type '{0}' must have a public default constructor.");
18
19        public static void MustHaveDefaultConstructor(this ITypeSetVerifier<IDeclaration> verifier) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
20



21    }
22
23#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
24
25
26
27
28#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
29    internal class Fabric : ProjectFabric
30    {
31        public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
32



33    }
34
35#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
36
37
38    // This class has an implicit default constructor.
39    public class ValidClass1 { }
40
41    // This class has an explicit default constructor.
42    public class ValidClass2
43    {
44        public ValidClass2() { }
45    }
46
47    // This class does not havr any default constructor.
48    public class InvalidClass
49    {
50        public InvalidClass(int x) { }
51    }
52}
53