MetalamaConceptual documentationCreating aspectsCreating simple aspectsValidating parameters, fields and properties
Open sandboxFocusImprove this doc

Getting started: contracts

One of the most prevalent use cases of aspect-oriented programming is the creation of a custom attribute for the validation of fields, properties, or parameters to which it is applied. Examples include [NotNull] or [NotEmpty].

In Metalama, this can be achieved by using a contract. With a contract, you have the option to:

  • Throw an exception when the value does not meet a condition of your choosing, or
  • Normalize the received value (for instance, by trimming the whitespace of a string).

A contract, technically, is a segment of code that is injected after receiving or before sending a value. It can be utilized for more than just throwing exceptions or normalizing values.

The simple way: overriding the ContractAspect class

  1. Add the Metalama.Framework package to your project.

  2. Create a new class that derives from the ContractAspect abstract class. This class will function as a custom attribute, and it is common practice to name it with the Attribute suffix.

  3. Implement the Validate method in plain C#. This method will act as a template that defines how the aspect overrides the hand-written target method.

    In this template, the incoming value is represented by the parameter name value, irrespective of the actual name of the field or parameter.

    The nameof(value) expression will be substituted with the name of the target parameter.

  4. The aspect operates as a custom attribute. It can be added to any field, property, or parameter. To validate the return value of a method, use the following syntax: [return: MyAspect].

Example: null check

The most frequent use of contracts is to verify nullability. Here is the simplest example.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.SimpleNotNull
5{
6    public class NotNullAttribute : ContractAspect
7    {
8        public override void Validate( dynamic? value )
9        {
10            if ( value == null! )
11            {
12                throw new ArgumentNullException( nameof(value) );
13            }
14        }
15    }
16}
Source Code
1namespace Doc.SimpleNotNull
2{


3    public class TheClass
4    {
5        [NotNull]
6        public string Field = "Field";


7
8        [NotNull]


















9        public string Property { get; set; } = "Property";
10
11        public void Method( [NotNull] string parameter ) { }












12    }
13}
Transformed Code
1using System;
2
3namespace Doc.SimpleNotNull
4{
5    public class TheClass
6    {
7        private string _field = "Field";
8
9        [NotNull]
10        public string Field
11        {
12            get
13            {
14                return this._field;
15            }
16
17            set
18            {
19                if (value == null!)
20                {
21                    throw new ArgumentNullException(nameof(value));
22                }
23
24                this._field = value;
25            }
26        }
27
28        private string _property = "Property";
29
30        [NotNull]
31        public string Property
32        {
33            get
34            {
35                return this._property;
36            }
37
38            set
39            {
40                if (value == null!)
41                {
42                    throw new ArgumentNullException(nameof(value));
43                }
44
45                this._property = value;
46            }
47        }
48        public void Method([NotNull] string parameter)
49        {
50            if (parameter == null!)
51            {
52                throw new ArgumentNullException(nameof(parameter));
53            }
54        }
55    }
56}

Observe how the nameof(value) expression is replaced by nameof(parameter) when the contract is applied to a parameter.

Example: trimming

A contract can be used for more than just throwing an exception. In the subsequent example, the aspect trims whitespace from strings. The same aspect is added to properties and parameters.

1using Metalama.Framework.Aspects;
2
3namespace Doc.Trim
4{
5    internal class TrimAttribute : ContractAspect
6    {
7        public override void Validate( dynamic? value )
8        {
9#pragma warning disable IDE0059 // Unnecessary assignment of a value
10            value = value?.Trim();
11#pragma warning restore IDE0059 // Unnecessary assignment of a value
12        }
13    }
14}
Source Code
1using System;
2
3namespace Doc.Trim
4{
5    internal class Foo
6    {
7        public void Method1( [Trim] string nonNullableString, [Trim] string? nullableString )
8        {
9            Console.WriteLine( $"nonNullableString='{nonNullableString}', nullableString='{nullableString}'" );
10        }
11


        Warning CS8618: Non-nullable property 'Property' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

12        public string Property { get; set; }
13    }
14
15    internal class Program
16    {
17        public static void Main()
18        {
19            var foo = new Foo();
20            foo.Method1( "     A  ", "   B " );
21            foo.Property = "    C   ";
22            Console.WriteLine( $"Property='{foo.Property}'" );
23        }
24    }
25}
Transformed Code
1using System;
2
3namespace Doc.Trim
4{
5    internal class Foo
6    {
7        public void Method1([Trim] string nonNullableString, [Trim] string? nullableString)
8        {
9            nonNullableString = nonNullableString.Trim();
10            nullableString = nullableString?.Trim();
11            Console.WriteLine($"nonNullableString='{nonNullableString}', nullableString='{nullableString}'");
12        }
13
14        public string Property { get; set; }
15    }
16
17    internal class Program
18    {
19        public static void Main()
20        {
21            var foo = new Foo();
22            foo.Method1("     A  ", "   B ");
23            foo.Property = "    C   ";
24            Console.WriteLine($"Property='{foo.Property}'");
25        }
26    }
27}
nonNullableString='A', nullableString='B'
Property='    C   '

Going deeper

If you wish to delve deeper into contracts, consider referring to the following articles:

  • In this article, we have restricted ourselves to very basic contract implementations. To learn how to write more complex code templates, you can directly refer to Writing T# templates.
  • In this article, we have only applied contracts to the default direction of fields, properties, or parameters. To understand the concept of contract direction, refer to Validating parameter, field, and property values with contracts.