Open sandboxFocusImprove this doc

Validating parameter, field, and property values with contracts

In Getting started: contracts, you learned how to create simple contracts by implementing the ContractAspect class.

This article covers more advanced scenarios.

Accessing the metadata of the field, property, or parameter being validated

You can access your template code's context using the following meta APIs:

  • meta.Target.Declaration returns the target parameter, property, or field.
  • meta.Target.FieldOrProperty returns the target property or field. However, it will throw an exception if the contract is applied to a parameter.
  • meta.Target.Parameter returns the parameter (including the parameter representing the return value). It will throw an exception if the contract is applied to a field or property.
  • meta.Target.ContractDirection returns Input or Output according to the data flow being validated (see below). Typically, it is Input for input parameters and property setters, and Output for output parameters and return values.

Contract directions

By default, the ContractAspect aspect applies the contract to the default data flow of the target parameter, field, or property.

The default direction is as follows:

  • For input and ref parameters: the input value.
  • For fields and properties: the assigned value (i.e., the value parameter of the setter).
  • For out parameters and return value parameters: the output value.

To change the filter direction, override the GetDefinedDirection method of the ContractAspect class.

For information on customizing eligibility for different contract directions than the default one, see the remarks in the documentation of the ContractAspect class. To learn about eligibility, visit Defining the eligibility of aspects.

Note

Prior to Metalama 2023.4, the GetDefinedDirection method did not exist. Instead, implementations could specify the contract direction in the ContractAspect constructor or set a property named Direction. Both this property and this constructor are now obsolete.

Example: NotNull for output parameters and return values

We previously encountered this aspect in Getting started: contracts. This example refines the behavior: for the input data flow, an ArgumentNullException is thrown. However, for the output flow, we throw a PostConditionFailedException. Notice how we apply the aspect to 'out' parameters and to return values.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.NotNull;
5
6internal class NotNullAttribute : ContractAspect
7{
8    public override void Validate( dynamic? value )
9    {
10        if ( value == null )
11        {
12            if ( meta.Target.ContractDirection == ContractDirection.Input )
13            {
14                throw new ArgumentNullException( nameof(value) );
15            }
16            else
17            {
18                throw new PostConditionFailedException(
19                    $"'{nameof(value)}' cannot be null when the method returns." );
20            }
21        }
22    }
23}
Source Code
1using System;
2
3namespace Doc.NotNull;
4
5internal class Foo
6{
7    public void Method1( [NotNull] string s ) { }
8
9    public void Method2( [NotNull] out string s )






10    {
11        s = null!;
12    }
13



14    [return: NotNull]

15    public string Method3()
16    {
17        return null!;
18    }




19



20    [NotNull]
21    public string Property { get; set; }


22}
23




24public class PostConditionFailedException : Exception
25{












26    public PostConditionFailedException( string message ) : base( message ) { }
27}
Transformed Code
1using System;
2
3namespace Doc.NotNull;
4
5internal class Foo
6{
7    public void Method1([NotNull] string s)
8    {
9        if (s == null)
10        {
11            throw new ArgumentNullException(nameof(s));
12        }
13    }
14
15    public void Method2([NotNull] out string s)
16    {
17        s = null!;
18        if (s == null)
19        {
20            throw new PostConditionFailedException($"'{nameof(s)}' cannot be null when the method returns.");
21        }
22    }
23
24    [return: NotNull]
25    public string Method3()
26    {
27        string returnValue;
28        returnValue = null!;
29        if (returnValue == null)
30        {
31            throw new PostConditionFailedException($"'{nameof(returnValue)}' cannot be null when the method returns.");
32        }
33
34        return returnValue;
35    }
36
37    private string _property = default!;
38
39    [NotNull]
40    public string Property
41    {
42        get
43        {
44            return _property;
45        }
46
47        set
48        {
49            if (value == null)
50            {
51                throw new ArgumentNullException(nameof(value));
52            }
53
54            _property = value;
55        }
56    }
57}
58
59public class PostConditionFailedException : Exception
60{
61    public PostConditionFailedException(string message) : base(message) { }
62}

Adding contract advice programmatically

Like any advice, you can add a contract to a parameter, field, or property from your aspect's BuildAspect method using the AddContract method.

Note

When possible, provide all contracts to the same method from a single aspect. This approach yields better compile-time performance than using several aspects.

Example: automatic NotNull

The following snippet demonstrates how to automatically add precondition checks for all situations in the public API where a non-nullable parameter could receive a null value from a consumer.

The fabric adds a method-level aspect to all exposed methods. Then, the aspect adds individual contracts using the AddContract method.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Fabrics;
5using System;
6using System.Linq;
7
8namespace Doc.NotNullFabric;
9
10internal class NotNullAttribute : MethodAspect
11{
12    public override void BuildAspect( IAspectBuilder<IMethod> builder )
13    {
14        base.BuildAspect( builder );
15
16        foreach ( var parameter in builder.Target.Parameters.Where(
17                     p => p.RefKind is RefKind.None or RefKind.In
18                          && p.Type.IsNullable != true
19                          && p.Type.IsReferenceType == true ) )
20        {
21            builder.With( parameter )
22                .AddContract( nameof(this.Validate), args: new { parameterName = parameter.Name } );
23        }
24    }
25
26    [Template]
27    private void Validate( dynamic? value, [CompileTime] string parameterName )
28    {
29        if ( value == null )
30        {
31            throw new ArgumentNullException( parameterName );
32        }
33    }
34}
35
36internal class Fabric : ProjectFabric
37{
38    public override void AmendProject( IProjectAmender amender )
39    {
40        amender.SelectMany(
41                a => a.Types
42                    .Where( t => t.Accessibility == Accessibility.Public )
43                    .SelectMany( t => t.Methods )
44                    .Where( m => m.Accessibility == Accessibility.Public ) )
45            .AddAspect<NotNullAttribute>();
46    }
47}
Source Code
1namespace Doc.NotNullFabric;
2


3public class PublicType
4{
5    public void PublicMethod(
6        string notNullableString,
7        string? nullableString,
8        int? nullableInt ) { }
9}
Transformed Code
1using System;
2
3namespace Doc.NotNullFabric;
4
5public class PublicType
6{
7    public void PublicMethod(
8        string notNullableString,
9        string? nullableString,
10        int? nullableInt)
11    {
12        if (notNullableString == null)
13        {
14            throw new ArgumentNullException("notNullableString");
15        }
16    }
17}
Note

For a production-ready version of this use case, see Checking all non-nullable fields, properties and parameters.