Open sandboxFocusImprove this doc

LAMA0654: An overridden record member reads a property that the compiler reads through its backing field

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 property is declared with the 'field' keyword and its getter has a body, so the getter can return a value other than the one that the backing field holds, and the two implementations then differ. Declare the property explicitly with a backing field, or give it an automatic getter.

Cause

The C# compiler generates the Equals and GetHashCode methods of a record. These generated methods compare the backing fields of the properties, not the properties themselves. When an aspect overrides one of these methods, for instance with IntroduceMethod and OverrideStrategy, Metalama must write the original implementation as source code so that the aspect can call it with meta.Proceed(). Source code can't name the compiler-generated backing field of a property declared with the field keyword, so Metalama reads the property instead.

When the getter of such a property returns something else than the backing field, the original implementation that Metalama writes isn't the one that the compiler generates: it compares the values returned by the getter, not the values stored in the fields. Metalama reports LAMA0654 on the property. In the message, <Member> is the overridden method, <RecordType> is the record, and <Property> is the property.

Metalama doesn't report the warning when the getter is automatic, or when its body only returns field.

In the following example, the Price property rounds the value in its getter, and the LogEquals aspect overrides the Equals method of the record.

1using System;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.LAMA0654.Error;
6
7public class LogEqualsAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        // Override the Equals method that the C# compiler generates for the record.
12        builder.IntroduceMethod(
13            nameof(this.IntroducedEquals),
14            whenExists: OverrideStrategy.Override,
15            args: new { T = builder.Target } );
16    }
17
18    [Template( Name = "Equals" )]
19    public bool IntroducedEquals<[CompileTime] T>( T? other )
20    {
21        Console.WriteLine( "Comparing two records." );
22
23        return meta.Proceed();
24    }
25}
26
Source Code
1using System;
2
3namespace Doc.LAMA0654.Error;

4
5[LogEquals]
6public record Product( string Name )
7{
8    // The getter has a body, so the original Equals reads the backing field but the overridden one reads the property.
    Warning LAMA0654: 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 property is declared with the 'field' keyword and its getter has a body, so the getter can return a value other than the one that the backing field holds, and the two implementations then differ. Declare the property explicitly with a backing field, or give it an automatic getter.

9    public double Price
10    {
11        get => Math.Round( field, 2 );
12        set => field = value;
13    }
14}






15
Transformed Code
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0654.Error;
5
6[LogEquals]
7public record Product(string Name)
8{
9    // The getter has a body, so the original Equals reads the backing field but the overridden one reads the property.
10    public double Price
11    {
12        get => Math.Round(field, 2);
13        set => field = value;
14    }
15
16    public virtual bool Equals(Product? other)
17    {
18        Console.WriteLine("Comparing two records.");
19        return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.Name, other.Name) && EqualityComparer<double>.Default.Equals(this.Price, other.Price));
20    }
21}
22

Resolution

Make sure that reading the property is the same as reading its backing field:

  • Give the property an automatic getter, and move the logic to the setter or to another member.
  • Declare the property explicitly with a backing field of your own.

In the following example, the value is rounded in the setter, and the getter is automatic.

1using System;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.LAMA0654.Fixed;
6
7public class LogEqualsAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        // Override the Equals method that the C# compiler generates for the record.
12        builder.IntroduceMethod(
13            nameof(this.IntroducedEquals),
14            whenExists: OverrideStrategy.Override,
15            args: new { T = builder.Target } );
16    }
17
18    [Template( Name = "Equals" )]
19    public bool IntroducedEquals<[CompileTime] T>( T? other )
20    {
21        Console.WriteLine( "Comparing two records." );
22
23        return meta.Proceed();
24    }
25}
26
Source Code
1using System;
2
3namespace Doc.LAMA0654.Fixed;

4
5[LogEquals]
6public record Product( string Name )
7{
8    // The getter is automatic, so reading the property is the same as reading the backing field.
9    public double Price
10    {
11        get;
12        set => field = Math.Round( value, 2 );
13    }
14}






15
Transformed Code
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0654.Fixed;
5
6[LogEquals]
7public record Product(string Name)
8{
9    // The getter is automatic, so reading the property is the same as reading the backing field.
10    public double Price
11    {
12        get;
13        set => field = Math.Round(value, 2);
14    }
15
16    public virtual bool Equals(Product? other)
17    {
18        Console.WriteLine("Comparing two records.");
19        return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.Name, other.Name) && EqualityComparer<double>.Default.Equals(this.Price, other.Price));
20    }
21}
22

See also