MetalamaConceptual documentationUsing Metalama PatternsContractsValidating field, property and parameter values
Open sandboxFocusImprove this doc

Validating field, property, and parameter values

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:

  1. Add the Metalama.Patterns.Contracts package.
  2. Add one of the <xref:contract-types?text=contract attributes> to the fields, properties, or parameters you wish to validate.

Example: validating input values

Source Code
1using Metalama.Patterns.Contracts;
2

3namespace Doc.Contracts.Input
4{
5    public 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    }
37}
Transformed Code
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Input
5{
6    public class Customer
7    {
8        private string? _phone;
9
10        [Phone]
11        public string? Phone
12        {
13            get
14            {
15                return this._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                this._phone = value;
28            }
29        }
30
31        private string? _url;
32
33        [Url]
34        public string? Url
35        {
36            get
37            {
38                return this._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                this._url = value;
51            }
52        }
53
54        private int? _birthYear;
55
56        [Range(1900, 2100)]
57        public int? BirthYear
58        {
59            get
60            {
61                return this._birthYear;
62            }
63
64            set
65            {
66                if (value is < 1900 or > 2100)
67                {
68                    throw new ArgumentOutOfRangeException("The 'BirthYear' property must be in the range [1900, 2100].", "value");
69                }
70
71                this._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 this._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 ArgumentOutOfRangeException("value", "The 'LastName' property is required.");
98                    }
99                }
100
101                this._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 ArgumentOutOfRangeException("fullName", "The 'fullName' parameter is required.");
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    }
133}

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.

Source Code
1using Metalama.Patterns.Contracts;
2

3namespace Doc.Contracts.Inheritance
4{
5    public 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
23    public 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    }
41}
Transformed Code
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Inheritance
5{
6    public 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
24    public class Customer : ICustomer
25    {
26        private string? _phone;
27
28        public string? Phone
29        {
30            get
31            {
32                return this._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                this._phone = value;
45            }
46        }
47
48        private string? _url;
49
50        public string? Url
51        {
52            get
53            {
54                return this._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                this._url = value;
67            }
68        }
69
70        private int? _birthYear;
71
72        public int? BirthYear
73        {
74            get
75            {
76                return this._birthYear;
77            }
78
79            set
80            {
81                if (value is < 1900 or > 2100)
82                {
83                    throw new ArgumentOutOfRangeException("The 'BirthYear' property must be in the range [1900, 2100].", "value");
84                }
85
86                this._birthYear = value;
87            }
88        }
89
90        private string _firstName = default!;
91
92        public string FirstName
93        {
94            get
95            {
96                return this._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 ArgumentOutOfRangeException("value", "The 'FirstName' property is required.");
110                    }
111                }
112
113                this._firstName = value;
114            }
115        }
116
117        private string _lastName = default!;
118
119        public string LastName
120        {
121            get
122            {
123                return this._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 ArgumentOutOfRangeException("value", "The 'LastName' property is required.");
137                    }
138                }
139
140                this._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 ArgumentOutOfRangeException("firstName", "The 'firstName' parameter is required.");
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 ArgumentOutOfRangeException("lastName", "The 'lastName' parameter is required.");
167                }
168            }
169
170            this.FirstName = firstName;
171            this.LastName = lastName;
172        }
173    }
174}

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.

Source Code
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.ReturnValue
4{
5    public 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
13    public 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    }


27}
Transformed Code
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.ReturnValue
4{
5    public 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
13    public 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    }
35}

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.

Source Code
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.OutParameter
4{
5    public 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
12    public 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    }


30}
Transformed Code
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Contracts.OutParameter
4{
5    public 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
12    public 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    }
38}

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.

Source Code
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System.Text.RegularExpressions;
4
5namespace Doc.Contracts.RefParameter
6{
7    public interface IWordCounter
8    {
9        void CountWords( string text, [Positive( Direction = ContractDirection.Both )] ref int wordCount );
10    }
11
12    public class WordCounter : IWordCounter
13    {
14        public void CountWords( string text, ref int wordCount )
15        {




16            var regex = new Regex( @"\b\w+\b" );
17            wordCount += regex.Matches( text ).Count;





18        }
19    }
20}
Transformed Code
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System.Text.RegularExpressions;
4
5namespace Doc.Contracts.RefParameter
6{
7    public interface IWordCounter
8    {
9        void CountWords(string text, [Positive(Direction = ContractDirection.Both)] ref int wordCount);
10    }
11
12    public class WordCounter : IWordCounter
13    {
14        public void CountWords(string text, ref int wordCount)
15        {
16            if (wordCount is < 0)
17            {
18                throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.");
19            }
20
21            var regex = new Regex(@"\b\w+\b");
22            wordCount += regex.Matches(text).Count;
23            if (wordCount is < 0)
24            {
25                throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.");
26            }
27        }
28    }
29}

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.

Source Code
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Property
5{
6    public interface IItem
7    {
8        [NotEmpty]
9        string Key { get; }
10
11        [NotEmpty( Direction = ContractDirection.Both )]
12        string Value { get; set; }
13    }
14
15    public 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    }
27}
Transformed Code
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Contracts.Property
5{
6    public interface IItem
7    {
8        [NotEmpty]
9        string Key { get; }
10
11        [NotEmpty(Direction = ContractDirection.Both)]
12        string Value { get; set; }
13    }
14
15    public class Item : IItem
16    {
17        private readonly string _key = default!;
18
19        public string Key
20        {
21            get
22            {
23                var returnValue = this._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                this._key = value;
35            }
36        }
37
38        private string _value = default!;
39
40        public string Value
41        {
42            get
43            {
44                var returnValue = this._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                this._value = value;
61            }
62        }
63
64        public Item(string key, string value)
65        {
66            this.Key = key;
67            this.Value = value;
68        }
69    }
70}