MetalamaConceptual documentationVerifying architectureExtending the validation API
Open sandboxFocusImprove this doc

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 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:

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

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 )
32            => new MethodNamePredicate( builder, suffix );
33    }
34
35    // Here is how your new predicate can be used.


36    internal class Fabric : ProjectFabric


37    {




38        public override void AmendProject( IProjectAmender amender )
39        {
40            amender.Verify().SelectTypes( typeof(CofeeMachine) ).CanOnlyBeUsedFrom( r => r.MethodNameEndsWith( "Politely" ) );
41        }
42    }
43
44    // This is the class whose access are validated.



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

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


28#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
29
30
31
32    // This class exposes the predicate as an extension method. It is your public API.
33
34#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
35
36
37    [CompileTime]
38    public static class Extensions
39    {
40        public static ReferencePredicate MethodNameEndsWith(this ReferencePredicateBuilder? builder, string suffix) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
41    }
42

43#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
44
45
46
47    // Here is how your new predicate can be used.
48
49#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
50
51
52    internal class Fabric : ProjectFabric
53    {
54        public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
55    }
56


57#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
58
59
60
61    // This is the class whose access are validated.
62    internal static class CofeeMachine
63    {
64        public static void TurnOn() { }
65    }
66
67    internal class Bar
68    {
69        public static void OrderCoffee()
70        {
71            // Forbidden because the method name does not end with Politely.
72            CofeeMachine.TurnOn();
73        }
74
75        public static void OrderCoffeePolitely()
76        {
77            // Allowed.
78            CofeeMachine.TurnOn();
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 <xref:System.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
22            if ( defaultConstructor == null || defaultConstructor.Accessibility != Accessibility.Public )
23            {
24                builder.Diagnostics.Report( _warning.WithArguments( builder.Target ) );
25            }
26        }
27    }
28}
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}

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 in the 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 `ver