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.
Begin with run-time settings.
Changing the validation logic
Some contracts use 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.
There's currently no way 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 useful during the early phases of development. As the code stabilizes, you can disable them. 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. When you disable them, the code supporting these features won't 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.Framework.Options;
3using Metalama.Patterns.Contracts;
4
5namespace Doc.Invariants_Disable;
6
7public class Fabric : ProjectFabric
8{
9 public override void AmendProject( IProjectAmender amender )
10 {
11 amender.Select( c => c.GlobalNamespace.GetDescendant( "Doc.Invariants_Disable" )! )
12 .SetOptions( new ContractOptions { AreInvariantsEnabled = false } );
13 }
14}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Invariants_Disable
4{
5 namespace Invoicing
6 {
7 public class Invoice
8 {
9 public decimal Amount { get; set; }
10
11 [Range( 0, 100 )]
12 public int DiscountPercent { get; set; }
13
14 [Range( 0, 100 )]
15 public decimal DiscountAmount { get; set; }
16
17 public virtual decimal DiscountedAmount
18 => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
19
20 [Invariant]
21 private void CheckDiscounts()
22 {
23 if ( this.DiscountedAmount < 0 )
24 {
25 throw new PostconditionViolationException(
26 "The discounted amount cannot be negative." );
27 }
28 }
29 }
30 }
31
32 namespace Fulfillment
33 {
34 public class FulfillmentProcess
35 {
36 public bool IsStarted { get; set; }
37
38 public bool IsCompleted { get; set; }
39
40 [Invariant]
41 private void CheckState()
42 {
43 if ( this.IsCompleted && !this.IsStarted )
44 {
45 throw new PostconditionViolationException();
46 }
47 }
48 }
49 }
50}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Invariants_Disable
5{
6 namespace Invoicing
7 {
8 public class Invoice
9 {
10 public decimal Amount { get; set; }
11
12 private int _discountPercent;
13
14 [Range(0, 100)]
15 public int DiscountPercent
16 {
17 get
18 {
19 return _discountPercent;
20 }
21
22 set
23 {
24 if (value is < 0 or > 100)
25 {
26 throw new ArgumentOutOfRangeException("value", value, "The 'DiscountPercent' property must be in the range [0, 100].");
27 }
28
29 _discountPercent = value;
30 }
31 }
32
33 private decimal _discountAmount;
34
35 [Range(0, 100)]
36 public decimal DiscountAmount
37 {
38 get
39 {
40 return _discountAmount;
41 }
42
43 set
44 {
45 if (value is < 0M or > 100M)
46 {
47 throw new ArgumentOutOfRangeException("value", value, "The 'DiscountAmount' property must be in the range [0, 100].");
48 }
49
50 _discountAmount = value;
51 }
52 }
53
54 public virtual decimal DiscountedAmount
55 => (this.Amount * (100 - this.Amount) / 100m) - this.DiscountAmount;
56
57 [Invariant]
58 private void CheckDiscounts()
59 {
60 if (this.DiscountedAmount < 0)
61 {
62 throw new PostconditionViolationException(
63 "The discounted amount cannot be negative.");
64 }
65 }
66 }
67 }
68
69 namespace Fulfillment
70 {
71 public class FulfillmentProcess
72 {
73 public bool IsStarted { get; set; }
74
75 public bool IsCompleted { get; set; }
76
77 [Invariant]
78 private void CheckState()
79 {
80 if (this.IsCompleted && !this.IsStarted)
81 {
82 throw new PostconditionViolationException();
83 }
84 }
85 }
86 }
87}
Example: disabling preconditions for a type or method
The following example demonstrates granular control over contract generation. The fabric disables preconditions for the entire DisabledService class and also disables them for a specific method (UnvalidatedMethod) in MixedService while leaving other methods validated.
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.DisablePreconditions;
5
6public class Fabric : ProjectFabric
7{
8 public override void AmendProject( IProjectAmender amender )
9 {
10 // Disable preconditions for the entire DisabledService class.
11 amender
12 .SelectReflectionType( typeof(DisabledService) )
13 .ConfigureContracts( new ContractOptions { ArePreconditionsEnabled = false } );
14
15 // Disable preconditions for a specific method.
16 amender
17 .SelectReflectionType( typeof(MixedService) )
18 .SelectMany( t => t.Methods.OfName( "UnvalidatedMethod" ) )
19 .ConfigureContracts( new ContractOptions { ArePreconditionsEnabled = false } );
20 }
21}
22
1using Metalama.Patterns.Contracts;
2
3namespace Doc.DisablePreconditions;
4
5// Contracts are enabled for this class (default behavior).
6public class EnabledService
7{
8 public void Process( [NotNull] string input, [Range( 0, 100 )] int percent )
9 {
10 }
11}
12
13// Contracts are disabled for this class via the fabric.
14public class DisabledService
15{
16 public void Process( [NotNull] string input, [Range( 0, 100 )] int percent )
17 {
18 }
19}
20
21// Contracts are selectively disabled for specific methods.
22public class MixedService
23{
24 // Contracts enabled (default).
25 public void ValidatedMethod( [NotNull] string input )
26 {
27 }
28
29 // Contracts disabled for this method via the fabric.
30 public void UnvalidatedMethod( [NotNull] string input )
31 {
32 }
33}
34
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.DisablePreconditions;
5
6// Contracts are enabled for this class (default behavior).
7public class EnabledService
8{
9 public void Process([NotNull] string input, [Range(0, 100)] int percent)
10 {
11 if (input == null!)
12 {
13 throw new ArgumentNullException("input", "The 'input' parameter must not be null.");
14 }
15
16 if (percent is < 0 or > 100)
17 {
18 throw new ArgumentOutOfRangeException("percent", percent, "The 'percent' parameter must be in the range [0, 100].");
19 }
20 }
21}
22
23// Contracts are disabled for this class via the fabric.
24public class DisabledService
25{
26 public void Process([NotNull] string input, [Range(0, 100)] int percent)
27 {
28 }
29}
30
31// Contracts are selectively disabled for specific methods.
32public class MixedService
33{
34 // Contracts enabled (default).
35 public void ValidatedMethod([NotNull] string input)
36 {
37 if (input == null!)
38 {
39 throw new ArgumentNullException("input", "The 'input' parameter must not be null.");
40 }
41 }
42
43 // Contracts disabled for this method via the fabric.
44 public void UnvalidatedMethod([NotNull] string input)
45 {
46 }
47}
48
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 example, to localize them), 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.Fabrics;
2using Metalama.Framework.Options;
3using Metalama.Patterns.Contracts;
4
5// ReSharper disable StringLiteralTypo
6
7namespace Doc.Localize;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 {
13 amender.SetOptions( new ContractOptions { Templates = new FrenchTemplates() } );
14 }
15}
1using Metalama.Framework.Aspects;
2using Metalama.Patterns.Contracts;
3using System;
4
5// ReSharper disable StringLiteralTypo
6
7namespace Doc.Localize;
8
9internal class FrenchTemplates : ContractTemplates
10{
11 public override void OnPhoneContractViolated( dynamic? value, ContractContext context )
12 {
13 if ( context.Direction == ContractDirection.Input )
14 {
15 throw new ArgumentException(
16 "La valeur doit être un numéro de téléphone correct.",
17 context.TargetParameterName );
18 }
19 else
20 {
21 throw new PostconditionViolationException(
22 "La valeur doit être un numéro de téléphone correct." );
23 }
24 }
25}
1using Metalama.Patterns.Contracts;
2
3namespace Doc.Localize;
4
5public class Client
6{
7 [Phone]
8 public string? Telephone { get; set; }
9}
1using System;
2using Metalama.Patterns.Contracts;
3
4namespace Doc.Localize;
5
6public class Client
7{
8 private string? _telephone;
9
10 [Phone]
11 public string? Telephone
12 {
13 get
14 {
15 return _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 _telephone = value;
28 }
29 }
30}