Metalama allows you to override the three semantics of events: add, remove, and invoke.
To override an event, you can use one of the following approaches:
- Create an aspect class derived from the OverrideEventAspect class and override the OverrideAdd, OverrideRemove, and OverrideInvoke methods.
- Use the OverrideAccessors method from the
BuildAspectmethod.
Overriding the add and remove accessors
Overriding the add and remove accessors of events follows a similar process to overriding properties.
If you attempt to override a field-like event, it is transformed into an explicitly implemented event and its backing field — just as happens with automatic properties.
Example: Logging
The following example demonstrates overriding the add and remove accessors of events, without overriding the invoke operation. The example aspect logs the operation of adding and removing handlers to an event. It is applied to both a field-like and an explicitly-implemented event. You can compare the code transformation pattern.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.EventLogging;
5
6public class LogAttribute : OverrideEventAspect
7{
8 public override void OverrideAdd( dynamic handler )
9 {
10 Console.WriteLine( $"Adding handler {((Delegate) handler).Method}." );
11 meta.Proceed();
12 }
13
14 public override void OverrideRemove( dynamic handler )
15 {
16 Console.WriteLine( $"Removing handler {((Delegate) handler).Method}." );
17 meta.Proceed();
18 }
19}
1using System;
2
3namespace Doc.EventLogging;
4
5public class Camera
6{
7 private EventHandler? _lightingChanged;
8
9 // Field-like event.
10 [Log]
11 public event EventHandler? FocusChanged;
12
13 private void OnFocusChanged()
14 {
15 this.FocusChanged?.Invoke( this, EventArgs.Empty );
16 }
17
18 // Explicitly-implemented event.
19 [Log]
20 public event EventHandler? LightingChanged
21 {
22 add => this._lightingChanged += value;
23 remove => this._lightingChanged -= value;
24 }
25
26 private void OnLightingChanged()
27 {
28 this._lightingChanged?.Invoke( this, EventArgs.Empty );
29 }
30}
1using System;
2
3namespace Doc.EventLogging;
4
5public class Camera
6{
7 private EventHandler? _lightingChanged;
8
9 private event EventHandler? _focusChanged;
10
11 // Field-like event.
12 [Log]
13 public event EventHandler? FocusChanged
14 {
15 add
16 {
17 Console.WriteLine($"Adding handler {value.Method}.");
18 this._focusChanged += value;
19 }
20
21 remove
22 {
23 Console.WriteLine($"Removing handler {value.Method}.");
24 this._focusChanged -= value;
25 }
26 }
27
28 private void OnFocusChanged()
29 {
30 this._focusChanged?.Invoke(this, EventArgs.Empty);
31 }
32
33 // Explicitly-implemented event.
34 [Log]
35 public event EventHandler? LightingChanged
36 {
37 add
38 {
39 Console.WriteLine($"Adding handler {value.Method}.");
40 this._lightingChanged += value;
41 }
42 remove
43 {
44 Console.WriteLine($"Removing handler {value.Method}.");
45 this._lightingChanged -= value;
46 }
47 }
48
49 private void OnLightingChanged()
50 {
51 this._lightingChanged?.Invoke(this, EventArgs.Empty);
52 }
53}
Overriding the invoke operation
Most of the time, advising an event requires overriding its invoke operation. For instance, if you want to swallow exceptions in event handlers or execute events in a background thread, it's best to do so by overriding the invoke semantic.
To override the invoke semantic, implement the OverrideEventAspect.OverrideInvoke method or supply an invokeTemplate argument to the OverrideAccessors method.
Note
The OverrideInvoke advice is invoked once per event handler. If there are 3 event handlers and the event is invoked once, the OverrideInvoke advice will be invoked 3 times (see graph below).
Adding/removing event handlers from an advice
If you are writing an exception handling aspect, you'll want to unregister the event handler from the invoke template. You can do this by invoking the Remove method from the template, for instance:
meta.Target.Event.Remove( handler );
Warning
The Raise method is not implemented yet.
Limitations
- Delegate signatures with a non-
voidreturn type or withoutandrefparameters are not supported. - Using
meta.Target.Event.Raise()from theOverrideInvoketemplate is not supported. You must usemeta.Proceed(). - Only handlers added through the event's add and remove accessors will be intercepted by the raise advice. Handlers added differently, for instance those added directly to the event backing field, won't be intercepted.
Example: Safe events
The following aspect implements a "Fool me once, shame on you; fool me twice, shame on me" pattern that handles exceptions in each event handler individually and unregisters any unreliable handler.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.SafeEvent_;
5
6public class SafeEventAttribute : OverrideEventAspect
7{
8 public override dynamic? OverrideInvoke( dynamic handler )
9 {
10 try
11 {
12 return meta.Proceed();
13 }
14 catch ( Exception e )
15 {
16 // Log the error.
17 Console.WriteLine( e );
18
19 // Remove the faulted event handler.
20 meta.Target.Event.Remove( handler );
21
22 return null;
23 }
24 }
25}
1using System;
2
3namespace Doc.SafeEvent_;
4
5public class Camera
6{
7 private EventHandler? _lightingChanged;
8
9 // Field-like event.
10 [SafeEvent]
11 public event EventHandler? FocusChanged;
12
13 private void OnFocusChanged()
14 {
15 this.FocusChanged?.Invoke( this, EventArgs.Empty );
16 }
17
18 // Explicitly-implemented event.
19 [SafeEvent]
20 public event EventHandler? LightingChanged
21 {
22 add => this._lightingChanged += value;
23 remove => this._lightingChanged -= value;
24 }
25
26 private void OnLightingChanged()
27 {
28 this._lightingChanged?.Invoke( this, EventArgs.Empty );
29 }
30}
31
1using System;
2using Metalama.Framework.RunTime.Events;
3
4namespace Doc.SafeEvent_;
5
6public class Camera
7{
8 private static readonly DelegateEventAdapter<EventHandler, (object?, EventArgs), Camera> FocusChangedAdapter_0 = new(
9 static (handler, ref args, me) => me.FocusChanged_Invoke_SafeEvent(handler, ref args),
10 static b => (sender, e) => b.Invoke((sender, e)),
11 static (handler, me) => me.FocusChanged_SafeEvent += handler,
12 static (handler, me) => me.FocusChanged_SafeEvent -= handler
13 );
14 private static readonly DelegateEventAdapter<EventHandler, (object?, EventArgs), Camera> LightingChangedAdapter_0 = new(
15static (handler, ref args, me) => me.LightingChanged_Invoke_SafeEvent(handler, ref args),
16static b => (sender, e) => b.Invoke((sender, e)),
17static (handler, me) => me.LightingChanged_SafeEvent += handler,
18static (handler, me) => me.LightingChanged_SafeEvent -= handler
19);
20 private EventHandler? _lightingChanged;
21
22 private event EventHandler? _focusChanged;
23
24 private volatile EventBroker<EventHandler, (object?, EventArgs), Camera>? _focusChangedBroker;
25
26 // Field-like event.
27 [SafeEvent]
28 public event EventHandler? FocusChanged
29 {
30 add
31 {
32 EventBroker.EnsureInitialized(ref this._focusChangedBroker, FocusChangedAdapter_0, this);
33 this._focusChangedBroker.AddHandler(value);
34 }
35
36 remove
37 {
38 this._focusChangedBroker?.RemoveHandler(value);
39 }
40 }
41
42 private event EventHandler? FocusChanged_SafeEvent
43 {
44 add
45 {
46 this._focusChanged += value;
47 }
48
49 remove
50 {
51 this._focusChanged -= value;
52 }
53 }
54
55 private void FocusChanged_Invoke_SafeEvent(EventHandler? handler, ref (object? sender, EventArgs e) args)
56 {
57 try
58 {
59 handler.Invoke(args.sender, args.e);
60 return;
61 }
62 catch (Exception e)
63 {
64 Console.WriteLine(e);
65 _focusChanged -= handler;
66 return;
67 }
68 }
69 private void OnFocusChanged()
70 {
71 this._focusChanged?.Invoke(this, EventArgs.Empty);
72 }
73
74 private volatile EventBroker<EventHandler, (object?, EventArgs), Camera>? _lightingChangedBroker;
75
76 // Explicitly-implemented event.
77 [SafeEvent]
78 public event EventHandler? LightingChanged
79 {
80 add
81 {
82 EventBroker.EnsureInitialized(ref this._lightingChangedBroker, LightingChangedAdapter_0, this);
83 this._lightingChangedBroker.AddHandler(value);
84 }
85 remove
86 {
87 this._lightingChangedBroker?.RemoveHandler(value);
88 }
89 }
90
91 private event EventHandler? LightingChanged_Source { add => this._lightingChanged += value; remove => this._lightingChanged -= value; }
92
93 private event EventHandler? LightingChanged_SafeEvent
94 {
95 add
96 {
97 this.LightingChanged_Source += value;
98 }
99
100 remove
101 {
102 this.LightingChanged_Source -= value;
103 }
104 }
105
106 private void LightingChanged_Invoke_SafeEvent(EventHandler? handler, ref (object? sender, EventArgs e) args)
107 {
108 try
109 {
110 handler.Invoke(args.sender, args.e);
111 return;
112 }
113 catch (Exception e)
114 {
115 Console.WriteLine(e);
116 LightingChanged_Source -= handler;
117 return;
118 }
119 }
120 private void OnLightingChanged()
121 {
122 this._lightingChanged?.Invoke(this, EventArgs.Empty);
123 }
124}
125
The implementation pattern for event invoke operations is more complex than for other advice kinds, as explained below.
Implementation
Overriding the invoke operation requires a complex code transformation from the Metalama framework. Since the C# language has no standard way to raise an event, the only reliable way to intercept an event invocation is to insert a broker between the event implementation and the event handlers. The broker is implemented by the EventBroker<TDelegate, TArgs, TState> class. Event handlers are added to the broker, and the broker is added as a client of the original implementation.
flowchart TD
A[Client code invokes event] --> B[Event Implementation]
B --> D[EventBroker]
D --> OverrideInvoke1[OverrideInvoke]
D --> OverrideInvoke2[OverrideInvoke]
D --> OverrideInvoke3[OverrideInvoke]
subgraph "Advice"
OverrideInvoke1
OverrideInvoke2
OverrideInvoke3
end
OverrideInvoke1 --> E1
OverrideInvoke2 --> E2
OverrideInvoke3 --> E3
subgraph "Handlers"
E1[Handler #1]
E2[Handler #2]
E3[Handler #3]
end
Performance considerations
Unlike other advice kinds, advising event invoke operations might affect run-time performance:
- additional memory is required by the EventBroker<TDelegate, TArgs, TState> class (one broker instance per event and per instance of the class, unless the event is
static). - raising the event allocates short-term memory because EventBroker<TDelegate, TArgs, TState> relies on MulticastDelegate and its GetInvocationList() method.
- adding, removing, and raising events require additional type conversions (casts).
This overhead might affect performance for events called at a very high frequency, although high-frequency events are not a frequent use case of .NET events.