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 final decision is entirely up to you.
Adding invariants
To add an invariant to a class:
- Create a
void
, non-static
, and parameterless method in your class. This method should typically beprivate
. - Add the [Invariant] custom attribute to this method.
- Add the validation logic to this method and throw an InvariantViolationException in case of a violation.
Warning
An invariant method should not 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 providing discounts. These two properties are additive, but their sum cannot result in a negative price. The CheckDiscounts
method enforces this condition.
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Invariants
4{
5 public 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 => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
16
17 [Invariant]
18 private void CheckDiscounts()
19 {
20 if ( this.DiscountedAmount < 0 )
21 {
22 throw new PostconditionViolationException( "The discounted amount cannot be negative." );
23 }
24 }
25 }
26}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants
5{
6 public class Invoice
7 {
8 private decimal _amount;
9
10 public decimal Amount
11 {
12 get
13 {
14 return this._amount;
15 }
16
17 set
18 {
19 try
20 {
21 this._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 this._discountPercent;
39 }
40
41 set
42 {
43 try
44 {
45 if (value is < 0 or > 100)
46 {
47 throw new ArgumentOutOfRangeException("The 'DiscountPercent' property must be in the range [0, 100].", "value");
48 }
49
50 this._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 this._discountAmount;
68 }
69
70 set
71 {
72 try
73 {
74 if (value is < 0M or > 100M)
75 {
76 throw new ArgumentOutOfRangeException("The 'DiscountAmount' property must be in the range [0, 100].", "value");
77 }
78
79 this._discountAmount = value;
80 return;
81 }
82 finally
83 {
84 this.VerifyInvariants();
85 }
86 }
87 }
88
89 public virtual decimal DiscountedAmount => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
90
91 [Invariant]
92 private void CheckDiscounts()
93 {
94 if (this.DiscountedAmount < 0)
95 {
96 throw new PostconditionViolationException("The discounted amount cannot be negative.");
97 }
98 }
99
100 protected virtual void VerifyInvariants()
101 {
102 this.CheckDiscounts();
103 }
104 }
105}
Opting out from invariant checking
When a type has any invariant, the implementation of all public or internal methods in this type will be wrapped into a try...finally
block, and invariants will be verified from the finally
block.
To prevent a method from being enhanced with this invariant checking logic, you can use the [DoNotCheckInvariants] custom attribute.
Note that this will not 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.
Once this feature is enabled, there are two ways to suspend invariant enforcement:
- To disable enforcement while the entire method is executing, add the [SuspendInvariants] custom attribute to this method.
- To disable enforcement while a code snippet is executing, wrap this snippet in a call to
using ( this.SuspendInvariants() )
. TheSuspendInvariants
method is generated by the IsInvariantSuspensionSupported option.
Example: suspending invariants
The following example builds upon the previous one. We added a fabric to enable the IsInvariantSuspensionSupported option. The Invoice
method now has two new methods, UpdateDiscounts1
and UpdateDiscounts2
, which update DiscountPercent
and DiscountAmount
while suspending invariants.
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants_Suspend
5{
6 public class Fabric : ProjectFabric
7 {
8 public override void AmendProject( IProjectAmender amender )
9 {
10 amender.Outbound.SetOptions( new ContractOptions { IsInvariantSuspensionSupported = true } );
11 }
12 }
13}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Invariants_Suspend
4{
5 public 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 => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
16
17 [Invariant]
18 private void CheckDiscounts()
19 {
20 if ( this.DiscountedAmount < 0 )
21 {
22 throw new PostconditionViolationException( "The discounted amount cannot be negative." );
23 }
24 }
25
26 [SuspendInvariants]
27 public void UpdateDiscounts1( int percent, decimal amount )
28 {
29 this.DiscountAmount = amount;
30 this.DiscountPercent = percent;
31 }
32
33 public void UpdateDiscounts2( int percent, decimal amount )
34 {
35 using ( this.SuspendInvariants() )
36 {
37 this.DiscountAmount = amount;
38 this.DiscountPercent = percent;
39 }
40 }
41 }
42}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants_Suspend
5{
6 public partial class Invoice
7 {
8 private decimal _amount;
9
10 public decimal Amount
11 {
12 get
13 {
14 return this._amount;
15 }
16
17 set
18 {
19 try
20 {
21 this._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 this._discountPercent;
42 }
43
44 set
45 {
46 try
47 {
48 if (value is < 0 or > 100)
49 {
50 throw new ArgumentOutOfRangeException("The 'DiscountPercent' property must be in the range [0, 100].", "value");
51 }
52
53 this._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 this._discountAmount;
74 }
75
76 set
77 {
78 try
79 {
80 if (value is < 0M or > 100M)
81 {
82 throw new ArgumentOutOfRangeException("The 'DiscountAmount' property must be in the range [0, 100].", "value");
83 }
84
85 this._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 => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
99
100 [Invariant]
101 private void CheckDiscounts()
102 {
103 if (this.DiscountedAmount < 0)
104 {
105 throw new PostconditionViolationException("The discounted amount cannot be negative.");
106 }
107 }
108
109 [SuspendInvariants]
110 public void UpdateDiscounts1(int percent, decimal amount)
111 {
112 using (this.SuspendInvariants())
113 {
114 try
115 {
116 this.DiscountAmount = amount;
117 this.DiscountPercent = percent;
118 }
119 finally
120 {
121 if (!this.AreInvariantsSuspended())
122 {
123 this.VerifyInvariants();
124 }
125 }
126
127 return;
128 }
129 }
130
131 public void UpdateDiscounts2(int percent, decimal amount)
132 {
133 try
134 {
135 using (this.SuspendInvariants())
136 {
137 this.DiscountAmount = amount;
138 this.DiscountPercent = percent;
139 }
140
141 return;
142 }
143 finally
144 {
145 if (!this.AreInvariantsSuspended())
146 {
147 this.VerifyInvariants();
148 }
149 }
150 }
151
152 private readonly InvariantSuspensionCounter _invariantSuspensionCounter = new();
153
154 protected bool AreInvariantsSuspended()
155 {
156 return _invariantSuspensionCounter.AreInvariantsSuspended;
157 }
158
159 protected SuspendInvariantsCookie SuspendInvariants()
160 {
161 _invariantSuspensionCounter.Increment();
162 return new SuspendInvariantsCookie(_invariantSuspensionCounter);
163 }
164
165 protected virtual void VerifyInvariants()
166 {
167 this.CheckDiscounts();
168 }
169 }
170}