One of the most prevalent use cases of aspect-oriented programming is the creation of a custom attribute for the validation of fields, properties, or parameters to which it is applied. Examples include [NotNull]
or [NotEmpty]
.
In Metalama, this can be achieved by using a contract. With a contract, you have the option to:
- Throw an exception when the value does not meet a condition of your choosing, or
- Normalize the received value (for instance, by trimming the whitespace of a string).
A contract, technically, is a segment of code that is injected after receiving or before sending a value. It can be utilized for more than just throwing exceptions or normalizing values.
The simple way: overriding the ContractAspect class
Add the
Metalama.Framework
package to your project.Create a new class that derives from the ContractAspect abstract class. This class will function as a custom attribute, and it is common practice to name it with the
Attribute
suffix.Implement the Validate method in plain C#. This method will act as a template that defines how the aspect overrides the hand-written target method.
In this template, the incoming value is represented by the parameter name
value
, irrespective of the actual name of the field or parameter.The
nameof(value)
expression will be substituted with the name of the target parameter.The aspect operates as a custom attribute. It can be added to any field, property, or parameter. To validate the return value of a method, use the following syntax:
[return: MyAspect]
.
Example: null check
The most frequent use of contracts is to verify nullability. Here is the simplest example.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.SimpleNotNull;
5
6public class NotNullAttribute : ContractAspect
7{
8 public override void Validate( dynamic? value )
9 {
10 if ( value == null! )
11 {
12 throw new ArgumentNullException( nameof(value) );
13 }
14 }
15}
1namespace Doc.SimpleNotNull;
2
3public class TheClass
4{
5 [NotNull]
6 public string Field = "Field";
7
8 [NotNull]
9 public string Property { get; set; } = "Property";
10
11 public void Method( [NotNull] string parameter ) { }
12}
1using System;
2
3namespace Doc.SimpleNotNull;
4
5public class TheClass
6{
7 private string _field = "Field";
8
9 [NotNull]
10 public string Field
11 {
12 get
13 {
14 return _field;
15 }
16
17 set
18 {
19 if (value == null!)
20 {
21 throw new ArgumentNullException(nameof(value));
22 }
23
24 _field = value;
25 }
26 }
27
28 private string _property = "Property";
29
30 [NotNull]
31 public string Property
32 {
33 get
34 {
35 return _property;
36 }
37
38 set
39 {
40 if (value == null!)
41 {
42 throw new ArgumentNullException(nameof(value));
43 }
44
45 _property = value;
46 }
47 }
48 public void Method([NotNull] string parameter)
49 {
50 if (parameter == null!)
51 {
52 throw new ArgumentNullException(nameof(parameter));
53 }
54 }
55}
Observe how the nameof(value)
expression is replaced by nameof(parameter)
when the contract is applied to a parameter.
Example: trimming
A contract can be used for more than just throwing an exception. In the subsequent example, the aspect trims whitespace from strings. The same aspect is added to properties and parameters.
1using Metalama.Framework.Aspects;
2
3namespace Doc.Trim;
4
5internal class TrimAttribute : ContractAspect
6{
7 public override void Validate( dynamic? value )
8 {
9#pragma warning disable IDE0059 // Unnecessary assignment of a value
10 value = value?.Trim();
11#pragma warning restore IDE0059 // Unnecessary assignment of a value
12 }
13}
1using System;
2
3namespace Doc.Trim;
4
5internal class Foo
6{
7 public void Method1( [Trim] string nonNullableString, [Trim] string? nullableString )
8 {
9 Console.WriteLine(
10 $"nonNullableString='{nonNullableString}', nullableString='{nullableString}'" );
11 }
12
13 public string Property { get; set; }
14}
15
16internal class Program
17{
18 public static void Main()
19 {
20 var foo = new Foo();
21 foo.Method1( " A ", " B " );
22 foo.Property = " C ";
23 Console.WriteLine( $"Property='{foo.Property}'" );
24 }
25}
1using System;
2
3namespace Doc.Trim;
4
5internal class Foo
6{
7 public void Method1([Trim] string nonNullableString, [Trim] string? nullableString)
8 {
9 nonNullableString = nonNullableString.Trim();
10 nullableString = nullableString?.Trim();
11 Console.WriteLine(
12 $"nonNullableString='{nonNullableString}', nullableString='{nullableString}'");
13 }
14
15 public string Property { get; set; }
16}
17
18internal class Program
19{
20 public static void Main()
21 {
22 var foo = new Foo();
23 foo.Method1(" A ", " B ");
24 foo.Property = " C ";
25 Console.WriteLine($"Property='{foo.Property}'");
26 }
27}
nonNullableString='A', nullableString='B' Property=' C '
Going deeper
If you wish to go deeper into contracts, consider referring to the following articles:
- In this article, we have restricted ourselves to very basic contract implementations. To learn how to write more complex code templates, you can directly refer to Writing T# templates.
- In this article, we have only applied contracts to the default direction of fields, properties, or parameters. To understand the concept of contract direction, refer to Validating parameter, field, and property values with contracts.