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
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, set the Direction property of the ContractAspect class in the constructor.
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.
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{
6 internal 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( $"'{nameof(value)}' cannot be null when the method returns." );
19 }
20 }
21 }
22 }
23}
1using System;
2
3namespace Doc.NotNull
4{
5 internal 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]
Warning CS8618: Non-nullable property 'Property' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
21 public string Property { get; set; }
22 }
23
24 public class PostConditionFailedException : Exception
25 {
26 public PostConditionFailedException( string message ) : base( message ) { }
27 }
28}
1using System;
2
3namespace Doc.NotNull
4{
5 internal 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
38 private string _property = default!;
39
40 [NotNull]
41 public string Property
42 {
43 get
44 {
45 return this._property;
46 }
47
48 set
49 {
50 if (value == null)
51 {
52 throw new ArgumentNullException(nameof(value));
53 }
54
55 this._property = value;
56 }
57 }
58 }
59
60 public class PostConditionFailedException : Exception
61 {
62 public PostConditionFailedException(string message) : base(message) { }
63 }
64}
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.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System;
5using System.Linq;
6
7namespace Doc.NotNullFabric
8{
9 internal class NotNullAttribute : MethodAspect
10 {
11 public override void BuildAspect( IAspectBuilder<IMethod> builder )
12 {
13 base.BuildAspect( builder );
14
15 foreach ( var parameter in builder.Target.Parameters.Where(
16 p => p.RefKind is RefKind.None or RefKind.In
17 && p.Type.IsNullable != true
18 && p.Type.IsReferenceType == true ) )
19 {
20 builder.Advice.AddContract( parameter, nameof(this.Validate), args: new { parameterName = parameter.Name } );
21 }
22 }
23
24 [Template]
25 private void Validate( dynamic? value, [CompileTime] string parameterName )
26 {
27 if ( value == null )
28 {
29 throw new ArgumentNullException( parameterName );
30 }
31 }
32 }
33
34 internal class Fabric : ProjectFabric
35 {
36 public override void AmendProject( IProjectAmender amender )
37 {
38 amender.Outbound.SelectMany(
39 a => a.Types
40 .Where( t => t.Accessibility == Accessibility.Public )
41 .SelectMany( t => t.Methods )
42 .Where( m => m.Accessibility == Accessibility.Public ) )
43 .AddAspect<NotNullAttribute>();
44 }
45 }
46}
1namespace Doc.NotNullFabric
2{
3 public class PublicType
4 {
5 public void PublicMethod( string notNullableString, string? nullableString, int? nullableInt ) { }
6 }
7}
1using System;
2
3namespace Doc.NotNullFabric
4{
5 public class PublicType
6 {
7 public void PublicMethod(string notNullableString, string? nullableString, int? nullableInt)
8 {
9 if (notNullableString == null)
10 {
11 throw new ArgumentNullException("notNullableString");
12 }
13 }
14 }
15}