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 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
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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.

Source Code
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 )]
12        ref int wordCount );
13}
14
15public class WordCounter : IWordCounter
16{
17    public void CountWords( string text, ref int wordCount )
18    {




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





21    }
22}
Transformed Code
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 )]
12        ref int wordCount);
13}
14
15public class WordCounter : IWordCounter
16{
17    public void CountWords(string text, ref int wordCount)
18    {
19        if (wordCount < 0)
20        {
21            throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.", wordCount);
22        }
23
24        var regex = new Regex(@"\b\w+\b");
25        wordCount += regex.Matches(text).Count;
26        if (wordCount < 0)
27        {
28            throw new PostconditionViolationException("The 'wordCount' parameter must be greater than or equal to 0.", wordCount);
29        }
30    }
31}

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
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}
Transformed Code
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}