Change Tracking Example, Step 1: Getting Started
This article will create an aspect that automatically implements the IChangeTracking interface of the .NET Framework.
Before implementing any aspect, we must first discuss the implementation design of the pattern. The main design decision is that objects will not track their changes by default. Instead, they will have an IsTrackingChanges
property to control this behavior. Why? Because if we enable change tracking by default, we will need to reset the changes after object initialization has been completed, and we have no way to automate this. So, instead of asking users to write code that calls the AcceptChanges method after each object initialization, we ask them to set the IsTrackingChanges
property to true
if they are interested in change tracking. Therefore, we can define the ISwitchableChangeTracking
interface as follows:
1using System.ComponentModel;
2
3
4public interface ISwitchableChangeTracking : IChangeTracking
5{
6 /// <summary>
7 /// Gets or sets a value indicating whether the current object
8 /// is tracking its changes.
9 /// </summary>
10 bool IsTrackingChanges { get; set; }
11}
We want our aspect to generate the following code. In this example, we have a base class named Comment
and a derived class named ModeratedComment
.
1[TrackChanges]
2public partial class Comment
3{
4 public Guid Id { get; }
5 public string Author { get; set; }
6 public string Content { get; set; }
7
8 public Comment( Guid id, string author, string content )
9 {
10 this.Id = id;
11 this.Author = author;
12 this.Content = content;
13 }
14}
1using System;
2using System.ComponentModel;
3
4[TrackChanges]
5public partial class Comment: ISwitchableChangeTracking, IChangeTracking
6{
7 public Guid Id { get; }
8
9
10 private string _author = default!;
11 public string Author
12 {
13 get
14 {
15 return this._author;
16 }
17
18 set
19 {
20 if (value != this._author)
21 {
22 this._author = value;
23 this.OnChange();
24 }
25 }
26 }
27
28 private string _content = default!;
29 public string Content
30 {
31 get
32 {
33 return this._content;
34 }
35
36 set
37 {
38 if (value != this._content)
39 {
40 this._content = value;
41 this.OnChange();
42 }
43 }
44 }
45
46 public Comment( Guid id, string author, string content )
47 {
48 this.Id = id;
49 this.Author = author;
50 this.Content = content;
51 }
52
53 public bool IsChanged { get; private set; }
54
55 public bool IsTrackingChanges { get; set; }
56
57 public void AcceptChanges()
58 {
59 this.IsChanged = false;
60 }
61
62 protected void OnChange()
63 {
64 if (this.IsTrackingChanges)
65 {
66 this.IsChanged = true;
67 }
68 }
69}
70
1[TrackChanges]
2public class ModeratedComment : Comment
3{
4 public ModeratedComment( Guid id, string author, string content ) : base( id, author, content )
5 {
6 }
7
8 public bool? IsApproved { get; set; }
9}
1[TrackChanges]
2public class ModeratedComment : Comment
3{
4 public ModeratedComment( Guid id, string author, string content ) : base( id, author, content )
5 {
6 }
7
8
9 private bool? _isApproved;
10
11 public bool? IsApproved
12 {
13 get
14 {
15 return this._isApproved;
16 }
17
18 set
19 {
20 if (value != this._isApproved)
21 {
22 this._isApproved = value;
23 this.OnChange();
24 }
25 }
26 }
27}
Aspect Implementation
Our aspect implementation needs to perform two operations:
- Implement the
ISwitchableChangeTracking
interface (including the IChangeTracking system interface) unless the type already implements it; - Add an
OnChange
method that sets the IsChanged property totrue
if change tracking is enabled unless the type already contains such a method; - Override the setter of all fields and automatic properties to call
OnChange
.
Here is the complete implementation:
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4[Inheritable]
5public class TrackChangesAttribute : TypeAspect
6{
7 public override void BuildAspect( IAspectBuilder<INamedType> builder )
8 {
9 // Implement the ISwitchableChangeTracking interface.
10 builder.Advice.ImplementInterface( builder.Target, typeof(ISwitchableChangeTracking), OverrideStrategy.Ignore );
11
12 // Override all writable fields and automatic properties.
13 var fieldsOrProperties = builder.Target.FieldsAndProperties
14 .Where( f => !f.IsImplicitlyDeclared &&
15 f.IsAutoPropertyOrField == true &&
16 f.Writeability == Writeability.All );
17
18 foreach ( var fieldOrProperty in fieldsOrProperties )
19 {
20 builder.Advice.OverrideAccessors( fieldOrProperty, null, nameof(this.OverrideSetter) );
21 }
22 }
23
24 [InterfaceMember]
25 public bool IsChanged { get; private set; }
26
27 [InterfaceMember]
28 public bool IsTrackingChanges { get; set; }
29
30
31 [InterfaceMember]
32 public void AcceptChanges() => this.IsChanged = false;
33
34 [Introduce( WhenExists = OverrideStrategy.Ignore )]
35 protected void OnChange()
36 {
37 if ( this.IsTrackingChanges )
38 {
39 this.IsChanged = true;
40 }
41 }
42
43 [Template]
44 private void OverrideSetter( dynamic? value )
45 {
46 if ( value != meta.Target.Property.Value )
47 {
48 meta.Proceed();
49
50 this.OnChange();
51 }
52 }
53}
The TrackChangesAttribute
class is a type-level aspect, so it must derive from the TypeAspect class, which itself derives from Attribute.
The [Inheritable] at the top of the class indicates that the aspect should be inherited from the base class to derived classes. For further details, see Applying aspects to derived types.
The entry point of the aspect is the BuildAspect
method. Our implementation has two parts, two of the three operations that our aspect has to perform.
First, the BuildAspect
method calls the ImplementInterface method to add the ISwitchableChangeTracking
interface to the target type. It specifies the OverrideStrategy to Ignore
, indicating that the operation should be ignored if the target type already implements the interface. The ImplementInterface method requires the aspect class to contain the interface members, which should be annotated with the [InterfaceMember] custom attribute. The implementation of these members is trivial. For details about adding interfaces to types, see Implementing interfaces.
Then, the BuildAspect
method selects fields and automatic properties except readonly
fields and init
or get
-only automatic properties (we apply this condition using the expression f.Writeability == Writeability.All
). For all these fields and properties, we call the OverrideAccessors method using OverridePropertySetter
as a template for the new setter. For further details, see Overriding fields or properties.
Here is the template for the field/property setter. Note that it must be annotated with the [Template] custom attribute.
43 [Template]
44 private void OverrideSetter( dynamic? value )
45 {
46 if ( value != meta.Target.Property.Value )
47 {
48 meta.Proceed();
49
50 this.OnChange();
51 }
52 }
To introduce the OnChange
method (which is not part of the interface), we use the following code:
34 [Introduce( WhenExists = OverrideStrategy.Ignore )]
35 protected void OnChange()
36 {
37 if ( this.IsTrackingChanges )
38 {
39 this.IsChanged = true;
40 }
41 }
The OnChange
method has an [Introduce] custom attribute; therefore, Metalama will add this method to the target type. We again assign the Ignore
value to the WhenExists property to skip this step if the target type already contains this method.
Summary
In this first article, we created the first version of the TrackChanged
aspect, which already does a good job. The aspect also works with manual implementations of IChangeTracking as long as the OnChangemethod is available. But what if there is no
OnChange` method? In the following article, we will see how to report an error when this happens.