Configuring contracts
There are two types of configuration options: compile-time and run-time. Compile-time options affect code generation and must be configured using a fabric. Run-time settings, on the other hand, must be configured during application startup.
Let's begin with run-time settings.
Changing the validation logic
Some contracts utilize flexible run-time settings for their validation. These settings are modifiable properties of the ContractHelpers class.
Aspect | Property | Description |
---|---|---|
CreditCardAttribute | IsValidCreditCardNumber | The Func<string?, bool> validates the string as a credit card number. |
EmailAttribute | EmailRegex | A regular expression validates the string as an email address. |
UrlAttribute | UrlRegex | A regular expression validates the string as a URL. |
PhoneAttribute | PhoneRegex | A regular expression validates the string as a phone number. |
These properties can be set during the initialization sequence of your application, and any changes will affect the entire application.
Currently, there is no method to modify these settings for a specific namespace or type within your application.
Changing compile-time options
All other configurable options are compile-time ones, represented by the ContractOptions class. These options can be configured granularly from a fabric using the configuration framework described in Configuring aspects with fabrics.
Enabling and disabling contracts
Contracts are often only useful during the early phases of development. As the code stabilizes, they can be disabled. However, when a problem arises, it may be beneficial to re-enable them for troubleshooting.
The ContractOptions class provides three properties that allow you to enable or disable contracts for the entire project, or more specifically for a given namespace or type:
These options are enabled by default. If you disable them, the code supporting these features will not be generated.
Example: disabling invariants in a namespace
In the example below, we have invariants in two sub-namespaces: Invoicing
and Fulfillment
. We use a ProjectFabric and the ContractOptions to disable invariants for the Fulfillment
namespace.
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}
Changing the default inheritance or contract direction options
By default, contract inheritance is enabled and contract direction is set to Default. To change these default values, use the IsInheritable and Direction properties of the ContractOptions object.
Customizing the exception type or text
The default behavior of Metalama Contracts is to generate code that throws the default .NET exception with a hard-coded error message. This default behavior is implemented by the ContractTemplates class.
To customize the type of exceptions thrown or the exception messages (for instance, to localize them), you need to override the ContractTemplates class. Follow these steps:
- Create a class derived from the ContractTemplates.
- Override any or all templates. You may want to refer to the original source code of the ContractTemplates class for inspiration.
- Using a ProjectFabric or a NamespaceFabric, set the Templates property of the ContractOptions object.
Example: translating error messages
The following example demonstrates how to translate the exception messages into French.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3using Metalama.Patterns.Contracts;
4using System;
5
6// ReSharper disable StringLiteralTypo
7
8namespace Doc.Localize
9{
10 internal class Fabric : ProjectFabric
11 {
12 public override void AmendProject( IProjectAmender amender )
13 {
14 amender.Outbound.SetOptions( new ContractOptions { Templates = new FrenchTemplates() } );
15 }
16 }
17}
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System;
4
5// ReSharper disable StringLiteralTypo
6
7namespace Doc.Localize
8{
9 internal class FrenchTemplates : ContractTemplates
10 {
11 public override void OnPhoneContractViolated( dynamic? value )
12 {
13 if ( meta.Target.ContractDirection == ContractDirection.Input )
14 {
15 throw new ArgumentException( "La valeur doit être un numéro de téléphone correct.", TargetParameterName );
16 }
17 else
18 {
19 throw new PostconditionViolationException( "La valeur doit être un numéro de téléphone correct." );
20 }
21 }
22 }
23}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Localize
4{
5 public class Client
6 {
7 [Phone]
8 public string? Telephone { get; set; }
9 }
10}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Localize
5{
6 public class Client
7 {
8 private string? _telephone;
9
10 [Phone]
11 public string? Telephone
12 {
13 get
14 {
15 return this._telephone;
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("La valeur doit être un numéro de téléphone correct.", "value");
25 }
26
27 this._telephone = value;
28 }
29 }
30 }
31}