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
returnsInput
orOutput
according to the data flow being validated (see below). Typically, it isInput
for input parameters and property setters, andOutput
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}
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}
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}
1namespace Doc.NotNullFabric;
2
3public class PublicType
4{
5 public void PublicMethod(
6 string notNullableString,
7 string? nullableString,
8 int? nullableInt ) { }
9}
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.