| Severity | Error |
| Reported by | Metalama.Framework |
| Message | The original implementation of '<Member>' generated for record '<RecordType>' reads the backing field of the field-like event '<Event>', which the event no longer has. An aspect overrides the event with templates that do not call the original implementation, so the linker replaces the event by one that stores no handler and the generated body cannot be compiled. Call 'meta.Proceed()' in the templates that override the event. |
Cause
The Equals and GetHashCode methods that the C# compiler generates for a record compare the backing fields, including the field of each field-like event. When an aspect overrides one of these methods, Metalama writes the original implementation as source code so that the aspect can call it with meta.Proceed().
When an aspect overrides the accessors of a field-like event with templates that never call meta.Proceed(), the original implementation of the event isn't used, and the event keeps no backing field. The original implementation of Equals or GetHashCode then reads a field that doesn't exist, so it can't be compiled. Metalama reports LAMA0655 on the event. In the message, <Member> is the overridden method, <RecordType> is the record, and <Event> is the event.
In the following example, the LogEquals aspect overrides the Equals method of the record and the accessors of the Changed event, but the accessor template doesn't call meta.Proceed().
1using System;
2using System.Linq;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5
6namespace Doc.LAMA0655.Error;
7
8public class LogEqualsAttribute : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 // Override the Equals method that the C# compiler generates for the record.
13 builder.IntroduceMethod(
14 nameof(this.IntroducedEquals),
15 whenExists: OverrideStrategy.Override,
16 args: new { T = builder.Target } );
17
18 // Override the accessors of the event.
19 builder.With( builder.Target.Events.Single( e => e.Name == "Changed" ) )
20 .OverrideAccessors( nameof(this.OverrideAccessor), nameof(this.OverrideAccessor) );
21 }
22
23 [Template( Name = "Equals" )]
24 public bool IntroducedEquals<[CompileTime] T>( T? other )
25 {
26 Console.WriteLine( "Comparing two records." );
27
28 return meta.Proceed();
29 }
30
31 [Template]
32 public void OverrideAccessor()
33 {
34 Console.WriteLine( "Accessing the event." );
35 }
36}
37
1using System;
2
3namespace Doc.LAMA0655.Error;
4
5[LogEquals]
6public record Sensor( string Name )
7{
8 // The accessor templates of the aspect do not call meta.Proceed(), so the event has no backing field.
Error LAMA0655: The original implementation of 'Sensor.Equals(Sensor?)' generated for record 'Sensor' reads the backing field of the field-like event 'Sensor.Changed', which the event no longer has. An aspect overrides the event with templates that do not call the original implementation, so the linker replaces the event by one that stores no handler and the generated body cannot be compiled. Call 'meta.Proceed()' in the templates that override the event.
9 public event EventHandler? Changed;
10}
11
Resolution
Call meta.Proceed() in the templates that override the accessors of the event, so that the event keeps its backing field. If the aspect must discard the handlers of the event, don't override Equals or GetHashCode of the same record.
In the following example, the accessor template calls meta.Proceed().
1using System;
2using System.Linq;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5
6namespace Doc.LAMA0655.Fixed;
7
8public class LogEqualsAttribute : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 // Override the Equals method that the C# compiler generates for the record.
13 builder.IntroduceMethod(
14 nameof(this.IntroducedEquals),
15 whenExists: OverrideStrategy.Override,
16 args: new { T = builder.Target } );
17
18 // Override the accessors of the event.
19 builder.With( builder.Target.Events.Single( e => e.Name == "Changed" ) )
20 .OverrideAccessors( nameof(this.OverrideAccessor), nameof(this.OverrideAccessor) );
21 }
22
23 [Template( Name = "Equals" )]
24 public bool IntroducedEquals<[CompileTime] T>( T? other )
25 {
26 Console.WriteLine( "Comparing two records." );
27
28 return meta.Proceed();
29 }
30
31 [Template]
32 public void OverrideAccessor()
33 {
34 Console.WriteLine( "Accessing the event." );
35 meta.Proceed();
36 }
37}
38
1using System;
2
3namespace Doc.LAMA0655.Fixed;
4
5[LogEquals]
6public record Sensor( string Name )
7{
8 // The accessor templates of the aspect call meta.Proceed(), so the event keeps its backing field.
9 public event EventHandler? Changed;
10}
11
1using System;
2using System.Collections.Generic;
3
4namespace Doc.LAMA0655.Fixed;
5
6[LogEquals]
7public record Sensor(string Name)
8{
9 private event EventHandler? _changed;
10
11 // The accessor templates of the aspect call meta.Proceed(), so the event keeps its backing field.
12 public event EventHandler? Changed
13 {
14 add
15 {
16 Console.WriteLine("Accessing the event.");
17 this._changed += value;
18 }
19
20 remove
21 {
22 Console.WriteLine("Accessing the event.");
23 this._changed -= value;
24 }
25 }
26
27 public virtual bool Equals(Sensor? other)
28 {
29 Console.WriteLine("Comparing two records.");
30 return (object)this == (object?)other || ((object?)other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.Name, other.Name) && EqualityComparer<EventHandler?>.Default.Equals(this._changed, other._changed));
31 }
32}
33