Open sandboxFocusImprove this doc

Getting started with contracts

One of the most common use cases of aspect-oriented programming is creating a custom attribute for validating fields, properties, or parameters. Examples include [NotNull] or [NotEmpty].

In Metalama, you can achieve this using a contract. With a contract, you can:

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

A contract is a segment of code injected after receiving or before sending a value. You can use it 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. The class functions as a custom attribute, so name it with the Attribute suffix.

  3. Implement the Validate method in plain C#. This method acts 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, regardless of the actual name of the field or parameter.

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

  4. The aspect operates as a custom attribute. Add it 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 common use of contracts is to verify nullability. Here's the simplest example.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.SimpleNotNull;
5
6public 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}
Source Code
1namespace Doc.SimpleNotNull;
2


3public 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}
Transformed Code
1using System;
2
3namespace Doc.SimpleNotNull;
4
5public class TheClass
6{
7    private string _field = "Field";
8
9    [NotNull]
10    public string Field
11    {
12        get
13        {
14            return _field;
15        }
16
17        set
18        {
19            if (value == null!)
20            {
21                throw new ArgumentNullException(nameof(value));
22            }
23
24            _field = value;
25        }
26    }
27
28    private string _property = "Property";
29
30    [NotNull]
31    public string Property
32    {
33        get
34        {
35            return _property;
36        }
37
38        set
39        {
40            if (value == null!)
41            {
42                throw new ArgumentNullException(nameof(value));
43            }
44
45            _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}

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

Example: trimming

You can use contracts for more than just throwing exceptions. In the following 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
5internal class TrimAttribute : ContractAspect
6{
7    public override void Validate( dynamic? value )
8    {
9        if ( meta.Target.Expression.Type.IsNullable == true )
10        {
11            value = value?.Trim();
12        }
13        else
14        {
15            value = value!.Trim();
16        }
17    }
18}
Source Code
1using System;
2
3namespace Doc.Trim;
4
5internal class Foo
6{
7    public void Method1( [Trim] string nonNullableString, [Trim] string? nullableString )
8    {
9        Console.WriteLine(
10            $"nonNullableString='{nonNullableString}', nullableString='{nullableString}'" );


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

Going deeper

To go deeper into contracts, refer to the following articles: