Open sandboxFocusImprove this doc

Checking type invariants

Invariants are methods that verify the consistency of the state of the current object and throw an exception if inconsistencies are found. We recommend throwing an exception of type InvariantViolationException, but the decision is entirely up to you.

Adding invariants

To add an invariant to a class:

  1. Create a void, non-static, and parameterless method in your class. This method should typically be private.
  2. Add the [Invariant] custom attribute to this method.
  3. Add the validation logic to this method and throw an InvariantViolationException in case of a violation.
Warning

An invariant method shouldn't have any side effects other than throwing an exception in case of an invariant violation.

Example: adding invariants

In the following example, an Invoice entity has two properties, DiscountPercent and DiscountAmount, that allow discounts. These two properties are additive, but their sum can't result in a negative price. The CheckDiscounts method enforces this condition.

Source Code
1using Metalama.Patterns.Contracts;
2

3namespace Doc.Invariants;
4
5public class Invoice
6{
7    public decimal Amount { get; set; }
8


9    [Range( 0, 100 )]





10    public int DiscountPercent { get; set; }
11
















12    [Range( 0, 100 )]





13    public decimal DiscountAmount { get; set; }
14





















15    public virtual decimal DiscountedAmount





16        => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;



















17
18    [Invariant]
19    private void CheckDiscounts()
20    {
21        if ( this.DiscountedAmount < 0 )
22        {
23            throw new PostconditionViolationException(
24                "The discounted amount cannot be negative." );
25        }
26    }

27}
Transformed Code
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants;
5
6public class Invoice
7{
8    private decimal _amount;
9
10    public decimal Amount
11    {
12        get
13        {
14            return _amount;
15        }
16
17        set
18        {
19            try
20            {
21                _amount = value;
22                return;
23            }
24            finally
25            {
26                this.VerifyInvariants();
27            }
28        }
29    }
30
31    private int _discountPercent;
32
33    [Range(0, 100)]
34    public int DiscountPercent
35    {
36        get
37        {
38            return _discountPercent;
39        }
40
41        set
42        {
43            try
44            {
45                if (value is < 0 or > 100)
46                {
47                    throw new ArgumentOutOfRangeException("value", value, "The 'DiscountPercent' property must be in the range [0, 100].");
48                }
49
50                _discountPercent = value;
51                return;
52            }
53            finally
54            {
55                this.VerifyInvariants();
56            }
57        }
58    }
59
60    private decimal _discountAmount;
61
62    [Range(0, 100)]
63    public decimal DiscountAmount
64    {
65        get
66        {
67            return _discountAmount;
68        }
69
70        set
71        {
72            try
73            {
74                if (value is < 0M or > 100M)
75                {
76                    throw new ArgumentOutOfRangeException("value", value, "The 'DiscountAmount' property must be in the range [0, 100].");
77                }
78
79                _discountAmount = value;
80                return;
81            }
82            finally
83            {
84                this.VerifyInvariants();
85            }
86        }
87    }
88
89    public virtual decimal DiscountedAmount
90        => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
91
92    [Invariant]
93    private void CheckDiscounts()
94    {
95        if (this.DiscountedAmount < 0)
96        {
97            throw new PostconditionViolationException(
98                "The discounted amount cannot be negative.");
99        }
100    }
101
102    protected virtual void VerifyInvariants()
103    {
104        CheckDiscounts();
105    }
106}

Opting out of invariant checking

When a type has any invariant, the implementation of all public or internal methods in this type is wrapped in a try...finally block, and invariants are verified from the finally block.

To prevent a method from being enhanced with this invariant checking logic, use the [DoNotCheckInvariants] custom attribute.

Note that this won't waive the enforcement of invariants in methods called by the target of the [DoNotCheckInvariants] attribute. Therefore, this attribute is mainly useful to optimize performance, not to relax invariants.

Suspending enforcement of invariants

If you have a code snippet that temporarily breaks invariants, you can suspend invariant enforcement.

First, enable the IsInvariantSuspensionSupported option for this type. This option is disabled by default because it generates additional code. You can set this option from a ProjectFabric or NamespaceFabric as described in Configuring contracts.

After enabling this feature, you can suspend invariant enforcement in two ways:

  • To disable enforcement while an entire method executes, add the [SuspendInvariants] custom attribute to the method.
  • To disable enforcement while a code snippet executes, wrap the snippet in a call to using (this.SuspendInvariants()). The SuspendInvariants method is generated by the IsInvariantSuspensionSupported option.

Example: suspending invariants

The following example builds upon the previous one. We've added a fabric to enable the IsInvariantSuspensionSupported option. The Invoice class now has two new methods, UpdateDiscounts1 and UpdateDiscounts2, which update DiscountPercent and DiscountAmount while suspending invariants.

1using Metalama.Framework.Fabrics;
2using Metalama.Framework.Options;
3using Metalama.Patterns.Contracts;
4
5namespace Doc.Invariants_Suspend;
6
7public class Fabric : ProjectFabric
8{
9    public override void AmendProject( IProjectAmender amender )
10    {
11        amender.SetOptions( new ContractOptions { IsInvariantSuspensionSupported = true } );
12    }
13}
Source Code
1using Metalama.Patterns.Contracts;
2

3namespace Doc.Invariants_Suspend;
4
5public partial class Invoice
6{
7    public decimal Amount { get; set; }
8


9    [Range( 0, 100 )]





10    public int DiscountPercent { get; set; }
11



















12    [Range( 0, 100 )]





























13    public decimal DiscountAmount { get; set; }
14
15    public virtual decimal DiscountedAmount





16        => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;






















17
18    [Invariant]
19    private void CheckDiscounts()
20    {
21        if ( this.DiscountedAmount < 0 )
22        {
23            throw new PostconditionViolationException(
24                "The discounted amount cannot be negative." );
25        }
26    }
27
28    [SuspendInvariants]
29    public void UpdateDiscounts1( int percent, decimal amount )
30    {
31        this.DiscountAmount = amount;
32        this.DiscountPercent = percent;




33    }
34
35    public void UpdateDiscounts2( int percent, decimal amount )







36    {
37            using ( this.SuspendInvariants() )
38            {





39                this.DiscountAmount = amount;
40                this.DiscountPercent = percent;
41            }
42    }
























43}
Transformed Code
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants_Suspend;
5
6public partial class Invoice
7{
8    private decimal _amount;
9
10    public decimal Amount
11    {
12        get
13        {
14            return _amount;
15        }
16
17        set
18        {
19            try
20            {
21                _amount = value;
22                return;
23            }
24            finally
25            {
26                if (!this.AreInvariantsSuspended())
27                {
28                    this.VerifyInvariants();
29                }
30            }
31        }
32    }
33
34    private int _discountPercent;
35
36    [Range(0, 100)]
37    public int DiscountPercent
38    {
39        get
40        {
41            return _discountPercent;
42        }
43
44        set
45        {
46            try
47            {
48                if (value is < 0 or > 100)
49                {
50                    throw new ArgumentOutOfRangeException("value", value, "The 'DiscountPercent' property must be in the range [0, 100].");
51                }
52
53                _discountPercent = value;
54                return;
55            }
56            finally
57            {
58                if (!this.AreInvariantsSuspended())
59                {
60                    this.VerifyInvariants();
61                }
62            }
63        }
64    }
65
66    private decimal _discountAmount;
67
68    [Range(0, 100)]
69    public decimal DiscountAmount
70    {
71        get
72        {
73            return _discountAmount;
74        }
75
76        set
77        {
78            try
79            {
80                if (value is < 0M or > 100M)
81                {
82                    throw new ArgumentOutOfRangeException("value", value, "The 'DiscountAmount' property must be in the range [0, 100].");
83                }
84
85                _discountAmount = value;
86                return;
87            }
88            finally
89            {
90                if (!this.AreInvariantsSuspended())
91                {
92                    this.VerifyInvariants();
93                }
94            }
95        }
96    }
97
98    public virtual decimal DiscountedAmount
99        => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
100
101    [Invariant]
102    private void CheckDiscounts()
103    {
104        if (this.DiscountedAmount < 0)
105        {
106            throw new PostconditionViolationException(
107                "The discounted amount cannot be negative.");
108        }
109    }
110
111    [SuspendInvariants]
112    public void UpdateDiscounts1(int percent, decimal amount)
113    {
114        using (this.SuspendInvariants())
115        {
116            try
117            {
118                this.DiscountAmount = amount;
119                this.DiscountPercent = percent;
120            }
121            finally
122            {
123                if (!this.AreInvariantsSuspended())
124                {
125                    this.VerifyInvariants();
126                }
127            }
128
129            return;
130        }
131    }
132    public void UpdateDiscounts2(int percent, decimal amount)
133    {
134        try
135        {
136            using (this.SuspendInvariants())
137            {
138                this.DiscountAmount = amount;
139                this.DiscountPercent = percent;
140            }
141
142            return;
143        }
144        finally
145        {
146            if (!this.AreInvariantsSuspended())
147            {
148                this.VerifyInvariants();
149            }
150        }
151    }
152
153    private readonly InvariantSuspensionCounter _invariantSuspensionCounter = new();
154
155    protected bool AreInvariantsSuspended()
156    {
157        return _invariantSuspensionCounter.AreInvariantsSuspended;
158    }
159
160    protected SuspendInvariantsCookie SuspendInvariants()
161    {
162        _invariantSuspensionCounter.Increment();
163        return new SuspendInvariantsCookie(_invariantSuspensionCounter);
164    }
165
166    protected virtual void VerifyInvariants()
167    {
168        CheckDiscounts();
169    }
170}