| 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. The backing field of an auto-property cannot be read from source code. The two implementations differ when a derived type overrides the property. Declare the property explicitly with a backing field, or make it non-overridable. |
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 must write the original implementation as source code. Source code can't read the backing field of an automatic property, so the generated body reads the property instead.
The two implementations behave the same unless a derived type overrides the property. Metalama reports LAMA0652 as a warning when a property of the record is virtual, or is an override that isn't sealed, because the generated body would then call the overriding implementation, whereas the compiler would read the field. The <Member> placeholder is the synthesized method, <RecordType> is the record, and <Property> is the overridable property.
In the following example, the TrackEquality aspect overrides the Equals method of the Product record, whose Price property is virtual. The warning is reported on the property.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.LAMA0652.Error;
7
8public class TrackEqualityAttribute : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 // Overriding the Equals method that the compiler synthesizes makes the linker generate its original implementation.
13 builder.IntroduceMethod( nameof(IntroducedEquals), whenExists: OverrideStrategy.Override, args: new { T = builder.Target } );
14 }
15
16 [Template( Name = "Equals" )]
17 public bool IntroducedEquals<[CompileTime] T>( T? other )
18 {
19 Console.WriteLine( "Comparing records." );
20
21 return meta.Proceed();
22 }
23}
24
1namespace Doc.LAMA0652.Error;
2
3[TrackEquality]
4public record Product
5{
6 public string Name { get; init; } = "";
7
8 // The property is virtual, so a derived type can override it.
Warning LAMA0652: The original implementation of 'Product.Equals(Product?)' generated for record 'Product' reads the property 'Product.Price', whereas the C# compiler reads its backing field. The backing field of an auto-property cannot be read from source code. The two implementations differ when a derived type overrides the property. Declare the property explicitly with a backing field, or make it non-overridable.
9 public virtual decimal Price { get; set; }
10}
11
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0652.Error;
5
6[TrackEquality]
7public record Product
8{
9 public string Name { get; init; } = "";
10
11 // The property is virtual, so a derived type can override it.
12 public virtual decimal Price { get; set; }
13
14 public virtual bool Equals(Product? other)
15 {
16 Console.WriteLine("Comparing records.");
17 return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.Name, other.Name) && EqualityComparer<decimal>.Default.Equals(this.Price, other.Price));
18 }
19}
20
Resolution
Do one of the following:
- Make the property non-overridable: remove the
virtualmodifier, or addsealedto anoverride. - Declare the property explicitly with a backing field, instead of an automatic property.
- If the generated implementation is acceptable for your derived types, ignore the warning.
In the following example, the Price property is no longer virtual.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.LAMA0652.Fixed;
7
8public class TrackEqualityAttribute : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 // Overriding the Equals method that the compiler synthesizes makes the linker generate its original implementation.
13 builder.IntroduceMethod( nameof(IntroducedEquals), whenExists: OverrideStrategy.Override, args: new { T = builder.Target } );
14 }
15
16 [Template( Name = "Equals" )]
17 public bool IntroducedEquals<[CompileTime] T>( T? other )
18 {
19 Console.WriteLine( "Comparing records." );
20
21 return meta.Proceed();
22 }
23}
24
1namespace Doc.LAMA0652.Fixed;
2
3[TrackEquality]
4public record Product
5{
6 public string Name { get; init; } = "";
7
8 // The property is not virtual.
9 public decimal Price { get; set; }
10}
11
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0652.Fixed;
5
6[TrackEquality]
7public record Product
8{
9 public string Name { get; init; } = "";
10
11 // The property is not virtual.
12 public decimal Price { get; set; }
13
14 public virtual bool Equals(Product? other)
15 {
16 Console.WriteLine("Comparing records.");
17 return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.Name, other.Name) && EqualityComparer<decimal>.Default.Equals(this.Price, other.Price));
18 }
19}
20