| Severity | Warning |
| Reported by | Metalama.Framework |
| Message | The original implementation of '<Member>' generated for record '<RecordType>' reads the property '<Property>', whereas the C# compiler reads its backing field. An aspect overrides the property with a template that does not call the original implementation, so the property has no backing field left and the generated body reads the value that the aspect returns instead of the value that the record stores. Call 'meta.Proceed()' in the template that overrides the property. |
Cause
The C# compiler synthesizes the Equals and GetHashCode methods of a record and reads the backing fields of its automatic properties. When an aspect overrides one of these methods and calls meta.Proceed(), Metalama writes the original implementation as source code, which reads the properties.
If the same aspect, or another one, overrides an automatic property of the record with a template that doesn't call meta.Proceed(), the property loses its backing field, and the value it returns no longer comes from the state of the record. The generated Equals or GetHashCode would then use the value that the aspect returns, not the stored value. Metalama reports LAMA0653 as a warning in this case. The <Member> placeholder is the synthesized method, <RecordType> is the record, and <Property> is the overridden property.
In the following example, the TrackQuantity aspect overrides both the Equals method and the Quantity property of the OrderLine record, but the getter template returns a constant. The warning is reported on the property.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.LAMA0653.Error;
8
9public class TrackQuantityAttribute : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
13 builder.IntroduceMethod( nameof(IntroducedEquals), whenExists: OverrideStrategy.Override, args: new { T = builder.Target } );
14 builder.With( builder.Target.Properties.Single( p => p.Name == "Quantity" ) ).Override( nameof(QuantityTemplate) );
15 }
16
17 [Template( Name = "Equals" )]
18 public bool IntroducedEquals<[CompileTime] T>( T? other )
19 {
20 return meta.Proceed();
21 }
22
23 [Template]
24 public dynamic? QuantityTemplate
25 {
26 get
27 {
28 // The template does not call the original implementation, so the property loses its backing field.
29 return 1;
30 }
31
32 set { }
33 }
34}
35
1namespace Doc.LAMA0653.Error;
2
3[TrackQuantity]
4public record OrderLine
5{
Warning LAMA0653: The original implementation of 'OrderLine.Equals(OrderLine?)' generated for record 'OrderLine' reads the property 'OrderLine.Quantity', whereas the C# compiler reads its backing field. An aspect overrides the property with a template that does not call the original implementation, so the property has no backing field left and the generated body reads the value that the aspect returns instead of the value that the record stores. Call 'meta.Proceed()' in the template that overrides the property.
6 public int Quantity { get; set; }
7}
8
1using System.Collections.Generic;
2
3namespace Doc.LAMA0653.Error;
4
5[TrackQuantity]
6public record OrderLine
7{
8 public int Quantity
9 {
10 get
11 {
12 return 1;
13 }
14
15 set
16 {
17 }
18 }
19
20 public virtual bool Equals(OrderLine? other)
21 {
22 return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(this.Quantity, other.Quantity));
23 }
24}
25
Resolution
Call meta.Proceed() in the getter and setter templates that override the property, so that the property keeps its backing field. If the aspect must replace the value of the property, consider whether the record should still use value-based equality.
In the following example, the templates call meta.Proceed().
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.LAMA0653.Fixed;
8
9public class TrackQuantityAttribute : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
13 builder.IntroduceMethod( nameof(IntroducedEquals), whenExists: OverrideStrategy.Override, args: new { T = builder.Target } );
14 builder.With( builder.Target.Properties.Single( p => p.Name == "Quantity" ) ).Override( nameof(QuantityTemplate) );
15 }
16
17 [Template( Name = "Equals" )]
18 public bool IntroducedEquals<[CompileTime] T>( T? other )
19 {
20 return meta.Proceed();
21 }
22
23 [Template]
24 public dynamic? QuantityTemplate
25 {
26 get
27 {
28 // The template calls the original implementation, so the property keeps its backing field.
29 Console.WriteLine( "Reading the quantity." );
30
31 return meta.Proceed();
32 }
33
34 set
35 {
36 meta.Proceed();
37 }
38 }
39}
40
1namespace Doc.LAMA0653.Fixed;
2
3[TrackQuantity]
4public record OrderLine
5{
6 public int Quantity { get; set; }
7}
8
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0653.Fixed;
5
6[TrackQuantity]
7public record OrderLine
8{
9 private int _quantity;
10
11 public int Quantity
12 {
13 get
14 {
15 Console.WriteLine("Reading the quantity.");
16 return _quantity;
17 }
18
19 set
20 {
21 _quantity = value;
22 }
23 }
24
25 public virtual bool Equals(OrderLine? other)
26 {
27 return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(this._quantity, other._quantity));
28 }
29}
30