Validating input values of fields, properties, or parameters (preconditions)
Most often, you will add contracts directly to their target field, property, or parameter using custom attributes.
Follow these simple steps:
- Add the
Metalama.Patterns.Contracts
package. - Add one of the contract attributes to the fields, properties, or parameters you wish to validate.
Example: validating input values
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.Input;
4
5public class Customer
6{
7 [Phone]
8 public string? Phone { get; set; }
9
10 [Url]
11 public string? Url { get; set; }
12
13 [Range( 1900, 2100 )]
14 public int? BirthYear { get; set; }
15
16 public string? FirstName { get; set; }
17
18 [Required]
19 public string LastName { get; set; }
20
21 public Customer( [Required] string fullName )
22 {
23 var split = fullName.Split( ' ' );
24
25 if ( split.Length == 0 )
26 {
27 this.FirstName = "";
28 this.LastName = split[0];
29 }
30 else
31 {
32 this.FirstName = split[0];
33 this.LastName = split[^1];
34 }
35 }
36}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Input;
5
6public class Customer
7{
8 private string? _phone;
9
10 [Phone]
11 public string? Phone
12 {
13 get
14 {
15 return _phone;
16 }
17
18 set
19 {
20 var regex = ContractHelpers.PhoneRegex;
21 if (value != null && !regex.IsMatch(value))
22 {
23 var regex_1 = regex;
24 throw new ArgumentException("The 'Phone' property must be a valid phone number.", "value");
25 }
26
27 _phone = value;
28 }
29 }
30
31 private string? _url;
32
33 [Url]
34 public string? Url
35 {
36 get
37 {
38 return _url;
39 }
40
41 set
42 {
43 var regex = ContractHelpers.UrlRegex;
44 if (value != null && !regex.IsMatch(value))
45 {
46 var regex_1 = regex;
47 throw new ArgumentException("The 'Url' property must be a valid URL.", "value");
48 }
49
50 _url = value;
51 }
52 }
53
54 private int? _birthYear;
55
56 [Range(1900, 2100)]
57 public int? BirthYear
58 {
59 get
60 {
61 return _birthYear;
62 }
63
64 set
65 {
66 if (value is < 1900 or > 2100)
67 {
68 throw new ArgumentOutOfRangeException("value", value, "The 'BirthYear' property must be in the range [1900, 2100].");
69 }
70
71 _birthYear = value;
72 }
73 }
74
75 public string? FirstName { get; set; }
76
77 private string _lastName = default!;
78
79 [Required]
80 public string LastName
81 {
82 get
83 {
84 return _lastName;
85 }
86
87 set
88 {
89 if (string.IsNullOrWhiteSpace(value))
90 {
91 if (value == null!)
92 {
93 throw new ArgumentNullException("value", "The 'LastName' property is required.");
94 }
95 else
96 {
97 throw new ArgumentException("The 'LastName' property is required.", "value");
98 }
99 }
100
101 _lastName = value;
102 }
103 }
104
105 public Customer([Required] string fullName)
106 {
107 if (string.IsNullOrWhiteSpace(fullName))
108 {
109 if (fullName == null!)
110 {
111 throw new ArgumentNullException("fullName", "The 'fullName' parameter is required.");
112 }
113 else
114 {
115 throw new ArgumentException("The 'fullName' parameter is required.", "fullName");
116 }
117 }
118
119 var split = fullName.Split(' ');
120
121 if (split.Length == 0)
122 {
123 this.FirstName = "";
124 this.LastName = split[0];
125 }
126 else
127 {
128 this.FirstName = split[0];
129 this.LastName = split[^1];
130 }
131 }
132}
Using contract inheritance
By default, all contracts are inherited from interfaces and virtual
or abstract
members to their implementation. This means that when you add a contract to an interface member, it will be automatically implemented in all classes implementing this interface. The same rule applies to virtual
or abstract
members.
Example: contract inheritance
In the following example, contracts are applied to members of the ICustomer
interface. You can observe that they are automatically implemented by the Customer
class that implements the interface.
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.Inheritance;
4
5public interface ICustomer
6{
7 [Phone]
8 string? Phone { get; set; }
9
10 [Url]
11 string? Url { get; set; }
12
13 [Range( 1900, 2100 )]
14 int? BirthYear { get; set; }
15
16 [Required]
17 string FirstName { get; set; }
18
19 [Required]
20 string LastName { get; set; }
21}
22
23public class Customer : ICustomer
24{
25 public string? Phone { get; set; }
26
27 public string? Url { get; set; }
28
29 public int? BirthYear { get; set; }
30
31 public string FirstName { get; set; }
32
33 public string LastName { get; set; }
34
35 public Customer( [Required] string firstName, [Required] string lastName )
36 {
37 this.FirstName = firstName;
38 this.LastName = lastName;
39 }
40}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Inheritance;
5
6public interface ICustomer
7{
8 [Phone]
9 string? Phone { get; set; }
10
11 [Url]
12 string? Url { get; set; }
13
14 [Range(1900, 2100)]
15 int? BirthYear { get; set; }
16
17 [Required]
18 string FirstName { get; set; }
19
20 [Required]
21 string LastName { get; set; }
22}
23
24public class Customer : ICustomer
25{
26 private string? _phone;
27
28 public string? Phone
29 {
30 get
31 {
32 return _phone;
33 }
34
35 set
36 {
37 var regex = ContractHelpers.PhoneRegex;
38 if (value != null && !regex.IsMatch(value))
39 {
40 var regex_1 = regex;
41 throw new ArgumentException("The 'Phone' property must be a valid phone number.", "value");
42 }
43
44 _phone = value;
45 }
46 }
47
48 private string? _url;
49
50 public string? Url
51 {
52 get
53 {
54 return _url;
55 }
56
57 set
58 {
59 var regex = ContractHelpers.UrlRegex;
60 if (value != null && !regex.IsMatch(value))
61 {
62 var regex_1 = regex;
63 throw new ArgumentException("The 'Url' property must be a valid URL.", "value");
64 }
65
66 _url = value;
67 }
68 }
69
70 private int? _birthYear;
71
72 public int? BirthYear
73 {
74 get
75 {
76 return _birthYear;
77 }
78
79 set
80 {
81 if (value is < 1900 or > 2100)
82 {
83 throw new ArgumentOutOfRangeException("value", value, "The 'BirthYear' property must be in the range [1900, 2100].");
84 }
85
86 _birthYear = value;
87 }
88 }
89
90 private string _firstName = default!;
91
92 public string FirstName
93 {
94 get
95 {
96 return _firstName;
97 }
98
99 set
100 {
101 if (string.IsNullOrWhiteSpace(value))
102 {
103 if (value == null!)
104 {
105 throw new ArgumentNullException("value", "The 'FirstName' property is required.");
106 }
107 else
108 {
109 throw new ArgumentException("The 'FirstName' property is required.", "value");
110 }
111 }
112
113 _firstName = value;
114 }
115 }
116
117 private string _lastName = default!;
118
119 public string LastName
120 {
121 get
122 {
123 return _lastName;
124 }
125
126 set
127 {
128 if (string.IsNullOrWhiteSpace(value))
129 {
130 if (value == null!)
131 {
132 throw new ArgumentNullException("value", "The 'LastName' property is required.");
133 }
134 else
135 {
136 throw new ArgumentException("The 'LastName' property is required.", "value");
137 }
138 }
139
140 _lastName = value;
141 }
142 }
143
144 public Customer([Required] string firstName, [Required] string lastName)
145 {
146 if (string.IsNullOrWhiteSpace(firstName))
147 {
148 if (firstName == null!)
149 {
150 throw new ArgumentNullException("firstName", "The 'firstName' parameter is required.");
151 }
152 else
153 {
154 throw new ArgumentException("The 'firstName' parameter is required.", "firstName");
155 }
156 }
157
158 if (string.IsNullOrWhiteSpace(lastName))
159 {
160 if (lastName == null!)
161 {
162 throw new ArgumentNullException("lastName", "The 'lastName' parameter is required.");
163 }
164 else
165 {
166 throw new ArgumentException("The 'lastName' parameter is required.", "lastName");
167 }
168 }
169
170 this.FirstName = firstName;
171 this.LastName = lastName;
172 }
173}
Validating output values (postconditions)
The most common use of code contracts is to validate the input data flow. This happens by default when you apply a contract to a field, property, or any parameter except out
ones. When you validate the input data flow, you are essentially being cautious and defensive against the code calling you. This is a best practice as it prevents defects of foreign components from causing unexplainable failures in your own component.
Validating the output data flow can also be useful. This is particularly beneficial when you distrust the implementation of some interface or virtual method. Therefore, it makes more sense to validate the output data flow when the constraint is applied to an interface or virtual member, and inheritance is used to enforce the constraint on implementations.
If the validation of the output data flow fails, an exception of type PostconditionViolationException is thrown.
Return values
To validate the return value of a method, apply the contract to the return parameter using the [return: XXX]
syntax.
Example: contract on return value
In the following example, a [NotEmpty]
contract has been added to the return value of the GetCustomerName
method in the ICustomerService
interface. The CustomerService
class implements this interface, and you can observe how the return value of the GetCustomerName
method implementation is being validated by the [NotEmpty]
contract.
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.ReturnValue;
4
5public interface ICustomerService
6{
7 // Returns the name of a given customer or null if it cannot be found,
8 // but never returns an empty string.
9 [return: NotEmpty]
10 public string? GetCustomerName( int id );
11}
12
13public class CustomerService : ICustomerService
14{
15 public string? GetCustomerName( int id )
16 {
17 if ( id == 1 )
18 {
19 return "Orontes I the Bactrian";
20 }
21 else
22 {
23 return null;
24 }
25 }
26}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.ReturnValue;
4
5public interface ICustomerService
6{
7 // Returns the name of a given customer or null if it cannot be found,
8 // but never returns an empty string.
9 [return: NotEmpty]
10 public string? GetCustomerName(int id);
11}
12
13public class CustomerService : ICustomerService
14{
15 public string? GetCustomerName(int id)
16 {
17 string? returnValue;
18 if (id == 1)
19 {
20 returnValue = "Orontes I the Bactrian";
21 }
22 else
23 {
24 returnValue = null;
25 }
26
27 if (returnValue != null && returnValue.Length <= 0)
28 {
29 throw new PostconditionViolationException("The return value must not be null or empty.");
30 }
31
32 return returnValue;
33 }
34}
Out parameters
To validate the value of an out
parameter just before the method exits, simply apply the custom attribute to the parameter as usual.
Example: contract on out parameter
In the following example, a [NotEmpty]
contract has been added to the out
parameter of the TryGetCustomerName
method in the ICustomerService
interface. The CustomerService
class implements this interface, and you can observe how the value of the out
parameter of the TryGetCustomerName
method implementation is being validated by the [NotEmpty]
contract.
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.OutParameter;
4
5public interface ICustomerService
6{
7 // Returns the name of a given customer or null if it cannot be found,
8 // but never returns an empty string.
9 bool TryGetCustomerName( int id, [NotEmpty] out string? name );
10}
11
12public class CustomerService : ICustomerService
13{
14 public bool TryGetCustomerName( int id, out string? name )
15 {
16 if ( id == 1 )
17 {
18 name = "Orontes I the Bactrian";
19
20 return true;
21 }
22 else
23 {
24 name = null;
25
26 return false;
27 }
28 }
29}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.OutParameter;
4
5public interface ICustomerService
6{
7 // Returns the name of a given customer or null if it cannot be found,
8 // but never returns an empty string.
9 bool TryGetCustomerName(int id, [NotEmpty] out string? name);
10}
11
12public class CustomerService : ICustomerService
13{
14 public bool TryGetCustomerName(int id, out string? name)
15 {
16 bool returnValue;
17 if (id == 1)
18 {
19 name = "Orontes I the Bactrian";
20
21 returnValue = true;
22 }
23 else
24 {
25 name = null;
26
27 returnValue = false;
28 }
29
30 if (name != null && name.Length <= 0)
31 {
32 throw new PostconditionViolationException("The 'name' parameter must not be null or empty.");
33 }
34
35 return returnValue;
36 }
37}
Ref parameters
By default, only the input value of ref
parameters is validated. To change the default behavior, use the Direction property. To validate only the output value, use the Output value. To validate both input and output values, use Both.
Example: contract on ref parameter
In the following example, a [Positive]
contract has been added to the ref
parameter of the CountWords
method of IWordCounter
. The Direction property is set to Both so that both the input and the output value of the parameter are verified. The WordCounter
class implements the IWordCounter
interface. You can observe that the [Positive]
contract is verified both when the method enters and completes.
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System.Text.RegularExpressions;
4
5namespace Doc.Contracts.RefParameter;
6
7public interface IWordCounter
8{
9 void CountWords(
10 string text,
11 [NonNegative( Direction = ContractDirection.Both )] ref int wordCount );
12}
13
14public class WordCounter : IWordCounter
15{
16 public void CountWords( string text, ref int wordCount )
17 {
18 var regex = new Regex( @"\b\w+\b" );
19 wordCount += regex.Matches( text ).Count;
20 }
21}
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System.Text.RegularExpressions;
4
5namespace Doc.Contracts.RefParameter;
6
7public interface IWordCounter
8{
9 void CountWords(
10 string text,
11 [NonNegative(Direction = ContractDirection.Both)] ref int wordCount);
12}
13
14public class WordCounter : IWordCounter
15{
16 public void CountWords(string text, ref int wordCount)
17 {
18 if (wordCount < 0)
19 {
20 throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.", wordCount);
21 }
22
23 var regex = new Regex(@"\b\w+\b");
24 wordCount += regex.Matches(text).Count;
25 if (wordCount < 0)
26 {
27 throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.", wordCount);
28 }
29 }
30}
Fields and properties
At first glance, it may seem surprising, but fields and properties also have an input and an output flow if you consider them from the right perspective. The input flow is the assignment one, i.e., the value passed to the setter, while the output flow is the one of the getter.
By default, when a contract is applied to a field or property that has a setter, the contract validates the value passed to the setter.
Just like with ref
parameters, you can use the Direction and set it to either Output or Both.
Example: output contracts on properties
In the following example, we have added the [NotEmpty]
contract to two properties of the IItem
interface. The Key
property is get-only, so the contract applies to the getter return value by default. The Value
property has both a getter and a setter, so we have set the Direction property to Both to validate both the input value and the output value.
The Item
class implements the IItem
interface. You can observe that the contracts defined on the IItem
interface are implemented in the code.
In the Item
class, the Key
property is implemented as an automatic property. It might seem surprising that the contract is still implemented in the getter instead of in the setter. The reason is to preserve the semantics of the contract: when applied to the getter, the contract promises to throw a PostconditionViolationException exception upon violation. Implementing the contract on the getter would change the contract. Specifically, no exception would be thrown if the property is never set.
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Property;
5
6public interface IItem
7{
8 [NotEmpty]
9 string Key { get; }
10
11 [NotEmpty( Direction = ContractDirection.Both )]
12 string Value { get; set; }
13}
14
15public class Item : IItem
16{
17 public string Key { get; }
18
19 public string Value { get; set; }
20
21 public Item( string key, string value )
22 {
23 this.Key = key;
24 this.Value = value;
25 }
26}
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Property;
5
6public interface IItem
7{
8 [NotEmpty]
9 string Key { get; }
10
11 [NotEmpty(Direction = ContractDirection.Both)]
12 string Value { get; set; }
13}
14
15public class Item : IItem
16{
17 private readonly string _key = default!;
18
19 public string Key
20 {
21 get
22 {
23 var returnValue = _key;
24 if (returnValue.Length <= 0)
25 {
26 throw new PostconditionViolationException("The 'Key' property must not be null or empty.");
27 }
28
29 return returnValue;
30 }
31
32 private init
33 {
34 _key = value;
35 }
36 }
37
38 private string _value = default!;
39
40 public string Value
41 {
42 get
43 {
44 var returnValue = _value;
45 if (returnValue.Length <= 0)
46 {
47 throw new PostconditionViolationException("The 'Value' property must not be null or empty.");
48 }
49
50 return returnValue;
51 }
52
53 set
54 {
55 if (value.Length <= 0)
56 {
57 throw new PostconditionViolationException("The 'Value' property must not be null or empty.");
58 }
59
60 _value = value;
61 }
62 }
63
64 public Item(string key, string value)
65 {
66 this.Key = key;
67 this.Value = value;
68 }
69}