MetalamaCommented examplesChange TrackingStep 2.​ Verifying manual code
Open sandboxFocusImprove this doc

Change Tracking example, step 2: verifying manual code

In the previous article, we created an aspect that automatically implements the IChangeTracking interface. If the base class has a manual implementation of the IChangeTracking, the aspect will still work correctly and call the OnChange method of the base class. However, what if the base class does not contain an OnChange method or if it is not protected? Let's improve the aspect and report an error in these situations.

The result of this aspect will be two new errors:

1namespace Metalama.Samples.Clone.Tests.MissingOnChangeMethod;
2
3[TrackChanges]
Error MY001: The 'ISwitchableChangeTracking' interface is implemented manually on type 'DerivedClass', but the type does not have an 'OnChange()' method.

4public class DerivedClass : BaseClass
5{
6}
7
8public class BaseClass : ISwitchableChangeTracking
9{
10    public bool IsChanged { get; protected set; }
11
12    public bool IsTrackingChanges { get; set; }
13
14    public void AcceptChanges()
15    {
16        if ( this.IsTrackingChanges )
17        {
18            this.IsChanged = false;
19        }
20    }
21
22    // Note that there is NO OnChange method.
23}
1namespace Metalama.Samples.Clone.Tests.OnChangeMethodNotProtected;
2
3[TrackChanges]
Error MY001: The 'ISwitchableChangeTracking' interface is implemented manually on type 'DerivedClass', but the type does not have an 'OnChange()' method.

4public class DerivedClass : BaseClass
5{
6}
7
8public class BaseClass : ISwitchableChangeTracking
9{
10    public bool IsChanged { get; protected set; }
11
12    public bool IsTrackingChanges { get; set; }
13
14    public void AcceptChanges()
15    {
16        if ( this.IsTrackingChanges )
17        {
18            this.IsChanged = false;
19        }
20    }
21
22
23    // Note that the OnChange method is private and not protected.
24    private void OnChange() => this.IsChanged = true;
25}

Aspect implementation

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5
6
7public class TrackChangesAttribute : TypeAspect
8{
9    private static readonly DiagnosticDefinition<INamedType> _mustHaveOnChangeMethod = new(
10        "MY001",
11        Severity.Error,
12        $"The '{nameof(ISwitchableChangeTracking)}' interface is implemented manually on type '{{0}}', but the type does not have an '{nameof(OnChange)}()' method.");
13
14    private static readonly DiagnosticDefinition _onChangeMethodMustBeProtected = new(
15        "MY002",
16        Severity.Error,
17        $"The '{nameof(OnChange)}()' method must be have the 'protected' accessibility.");
18
19    public override void BuildAspect( IAspectBuilder<INamedType> builder )
20    {
21        // Implement the ISwitchableChangeTracking interface.         
22        var implementInterfaceResult = builder.Advice.ImplementInterface( builder.Target,
23            typeof(ISwitchableChangeTracking), OverrideStrategy.Ignore ); 
24
25        // If the type already implements IChangeTracking, it must have a protected method called OnChanged, without parameters, otherwise
26        // this is a contract violation, so we report an error.
27        if ( implementInterfaceResult.Outcome == AdviceOutcome.Ignore )
28        {
29            var onChangeMethod = builder.Target.AllMethods.OfName( nameof(this.OnChange) )
30                .SingleOrDefault( m => m.Parameters.Count == 0 );
31
32            if ( onChangeMethod == null )
33            {
34                builder.Diagnostics.Report( _mustHaveOnChangeMethod.WithArguments( builder.Target ) );
35            }
36            else if ( onChangeMethod.Accessibility != Accessibility.Protected )
37            {
38                builder.Diagnostics.Report( _onChangeMethodMustBeProtected );
39            }
40        }
41        
42
43        // Override all writable fields and automatic properties.
44        var fieldsOrProperties = builder.Target.FieldsAndProperties
45            .Where( f =>
46                !f.IsImplicitlyDeclared && f.Writeability == Writeability.All && f.IsAutoPropertyOrField == true );
47
48        foreach ( var fieldOrProperty in fieldsOrProperties )
49        {
50            builder.Advice.OverrideAccessors( fieldOrProperty, null, nameof(this.OverrideSetter) );
51        }
52    }
53
54
55    [InterfaceMember]
56    public bool IsChanged { get; private set; }
57
58    [InterfaceMember]
59    public bool IsTrackingChanges { get; set; }
60
61
62    [InterfaceMember]
63    public void AcceptChanges() => this.IsChanged = false;
64
65    [Introduce( WhenExists = OverrideStrategy.Ignore )]
66    protected void OnChange()
67    {
68        if ( this.IsTrackingChanges )
69        {
70            this.IsChanged = true;
71        }
72    }
73
74    [Template]
75    private void OverrideSetter( dynamic? value )
76    {
77        if ( value != meta.Target.Property.Value )
78        {
79            meta.Proceed();
80
81            this.OnChange();
82        }
83    }
84}

The first thing we add to the TrackChangesAttribute is two static fields to define the errors:

9    private static readonly DiagnosticDefinition<INamedType> _mustHaveOnChangeMethod = new(
10        "MY001",
11        Severity.Error,
12        $"The '{nameof(ISwitchableChangeTracking)}' interface is implemented manually on type '{{0}}', but the type does not have an '{nameof(OnChange)}()' method.");
14    private static readonly DiagnosticDefinition _onChangeMethodMustBeProtected = new(
15        "MY002",
16        Severity.Error,
17        $"The '{nameof(OnChange)}()' method must be have the 'protected' accessibility.");

Metalama requires the DiagnosticDefinition to be defined in a static field or property. To learn more about reporting errors, see Reporting and suppressing diagnostics.

Then, we add this code to the BuildAspect method:

23            typeof(ISwitchableChangeTracking), OverrideStrategy.Ignore ); 
24
25        // If the type already implements IChangeTracking, it must have a protected method called OnChanged, without parameters, otherwise
26        // this is a contract violation, so we report an error.
27        if ( implementInterfaceResult.Outcome == AdviceOutcome.Ignore )
28        {
29            var onChangeMethod = builder.Target.AllMethods.OfName( nameof(this.OnChange) )
30                .SingleOrDefault( m => m.Parameters.Count == 0 );
31
32            if ( onChangeMethod == null )
33            {
34                builder.Diagnostics.Report( _mustHaveOnChangeMethod.WithArguments( builder.Target ) );
35            }
36            else if ( onChangeMethod.Accessibility != Accessibility.Protected )
37            {
38                builder.Diagnostics.Report( _onChangeMethodMustBeProtected );
39            }
40        }
41        

As in the previous step, the BuildAspect method calls ImplementInterface with the Ignore OverrideStrategy. This time, we inspect the outcome of ImplementInterface. If the outcome is Ignored, it means that the type or any base type already implements the IChangeTracking interface. In this case, we check that the type contains a parameterless method named OnChange and verify its accessibility.

Summary

This article explained how to report an error when the source code does not meet the expectations of the aspect. To make our aspect usable in practice, i.e., to make it possible to enable or disable a hypothetical Save button when the user performs changes in the UI, we still have to integrate with the INotifyPropertyChanged interface and raise the <xref:System.ComponentModel.INotifyPropertyChanged.PropertyChanged> event when the <xref:System.ComponentModel.IChangeTracking.IsChanged> property changes. We will see how to do this in the following article.