Open sandboxFocusImprove this doc

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.

Source Code



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}
Transformed Code
1using System;
2using System.ComponentModel;
3
4[TrackChanges]
5public partial class Comment
6: ISwitchableChangeTracking, IChangeTracking
7{
8    public Guid Id { get; }
9private string _author = default!;
10
11    public string Author { get { return this._author; } set { if (value != this._author) { this._author = value; OnChange(); } } }
12    private string _content = default!;
13
14    public string Content { get { return this._content; } set { if (value != this._content) { this._content = value; OnChange(); } } }
15
16    public Comment(Guid id, string author, string content)
17    {
18        this.Id = id;
19        this.Author = author;
20        this.Content = content;
21    }
22public bool IsChanged { get; private set; }
23    public bool IsTrackingChanges { get; set; }
24
25    public void AcceptChanges()
26    {
27        IsChanged = false;
28    }
29    protected void OnChange()
30    {
31        if (IsTrackingChanges)
32        {
33            IsChanged = true;
34        }
35    }
36}
Source Code
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}
Transformed Code
1[TrackChanges]
2public class ModeratedComment : Comment
3{
4    public ModeratedComment(Guid id, string author, string content) : base(id, author, content)
5    {
6    }
7private bool? _isApproved;
8
9    public bool? IsApproved { get { return this._isApproved; } set { if (value != this._isApproved) { this._isApproved = value; OnChange(); } } }
10}

Aspect Implementation

Our aspect implementation needs to perform two operations:

  1. Implement the ISwitchableChangeTracking interface (including the IChangeTracking system interface) unless the type already implements it;
  2. Add an OnChange method that sets the IsChanged property to true if change tracking is enabled unless the type already contains such a method;
  3. 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),
11            OverrideStrategy.Ignore);
12
13        // Override all writable fields and automatic properties.
14        var fieldsOrProperties = builder.Target.FieldsAndProperties
15            .Where(f => !f.IsImplicitlyDeclared &&
16                        f.IsAutoPropertyOrField == true &&
17                        f.Writeability == Writeability.All);
18
19        foreach (var fieldOrProperty in fieldsOrProperties)
20        {
21            builder.Advice.OverrideAccessors(fieldOrProperty, null, nameof(this.OverrideSetter));
22        }
23    }
24
25    [InterfaceMember] public bool IsChanged { get; private set; }
26
27    [InterfaceMember] public bool IsTrackingChanges { get; set; }
28
29
30    [InterfaceMember]
31    public void AcceptChanges() => this.IsChanged = false;
32
33    [Introduce(WhenExists = OverrideStrategy.Ignore)]
34    protected void OnChange()
35    {
36        if (this.IsTrackingChanges)
37        {
38            this.IsChanged = true;
39        }
40    }
41
42    [Template]
43    private void OverrideSetter(dynamic? value)
44    {
45        if (value != meta.Target.Property.Value)
46        {
47            meta.Proceed();
48
49            this.OnChange();
50        }
51    }
52}

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.

42[Template]
43private void OverrideSetter(dynamic? value)
44{
45    if (value != meta.Target.Property.Value)
46    {
47        meta.Proceed();
48
49        this.OnChange();
50    }
51}

To introduce the OnChange method (which is not part of the interface), we use the following code:

33[Introduce(WhenExists = OverrideStrategy.Ignore)]
34protected void OnChange()
35{
36    if (this.IsTrackingChanges)
37    {
38        this.IsChanged = true;
39    }
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 noOnChange` method? In the following article, we will see how to report an error when this happens.