RefactorMCP catalog

Add Observer / Event

Declares an event on a method's class and raises it whenever the method completes, so other objects can observe the change the method makes without the class knowing about them.

This is a generator: it adds a public member and a call to whatever handlers are subscribed, so the class's surface and, once something subscribes, its behaviour change. The after/ fixtures pin one design, described below, rather than the only correct answer.

Arguments

ArgumentRequiredMeaning
eventyesThe event's name, such as Updated

The target is the method, by symbol: "target": { "symbol": "M:Shop.Counter.Update(System.Int32)" }.

Precondition

  • The method has a body and returns void.
  • No parameter is ref, out or in; an Action cannot pass them.
  • The class has no member with the event's name.

Transformation

  • The event is public event Action<T1, ..., Tn> Name;, with the method's parameter types in order as they are written, or Action for a method without parameters. It is static when the method is, and declared nullable (Action<...>?) where nullable annotations are enabled.
  • The event is declared immediately before the method, taking the blank lines that preceded the method, with a blank line between the two. The method keeps its documentation comment.
  • Name?.Invoke(p1, ..., pn); passes the method's parameters and is raised:
  • at the end of the body when the end is reachable, after any comment that sits before the closing brace;
  • before every return that leaves the method; a return that is the embedded statement of an if, else or loop is wrapped in a block with the call.
  • An expression body becomes a block holding the expression as a statement.
  • using System; is added when Action does not bind in the file.

Preserved

  • The method's statements and what they do; the event is raised after them.
  • Returns inside lambdas and local functions, which leave something other than the method.
  • Behaviour, while nothing subscribes to the event.

Limitations

  • The event is raised on normal completion only, not when the method throws.
  • Handlers receive the parameters' values at the end of the method; a parameter the method reassigns is passed as reassigned.
  • A custom EventHandler<TEventArgs> with an arguments class is not generated; the event is an Action.
  • The event is always public, so a method whose parameter types are less accessible than its class is refused because the result would not compile.

Error codes

CodeMeaning
no-bodythe method is abstract, extern, partial without a body, or an interface member
not-voidthe method returns a value
ref-parametera parameter is ref, out or in
member-existsthe class already has a member with the event's name

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#comments-and-parameters

Passes every parameter to the event, keeps the method's documentation comment with the method, and raises after a trailing comment

success
Targetsymbol M:Shop.Account.Transfer(Shop.Account,System.Decimal,System.String)
Arguments
event"Transferred"
Account.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Account
66 {
77 public decimal Balance { get; private set; }
88
9+ public event Action<Account, decimal, string> Transferred;
10+
911 /// <summary>Moves money to another account.</summary>
1012 public void Transfer(Account to, decimal amount, string reference)
1113 {
1214 Balance -= amount;
1315 to.Balance += amount;
1416 // done
17+ Transferred?.Invoke(to, amount, reference);
1518 }
1619 }
1720 }

#early-returns

Raises the event before every return that leaves the method, wrapping an embedded return in a block, but not inside a lambda

success
Targetsymbol M:Shop.Basket.Clear
Arguments
event"Cleared"
Basket.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Basket
77 {
88 private readonly List<string> _items = new List<string>();
99
10+ public event Action Cleared;
11+
1012 public void Clear()
1113 {
1214 if (_items.Count == 0)
15+ {
16+ Cleared?.Invoke();
1317 return;
18+ }
1419
1520 _items.RemoveAll(item =>
1621 {
1722 return true;
1823 });
1924 if (_items.Count > 0)
2025 {
26+ Cleared?.Invoke();
2127 return;
2228 }
2329
2430 _items.Clear();
31+ Cleared?.Invoke();
2532 }
2633 }
2734 }

#member-exists

Refuses an event name the class already uses

refusal
Targetsymbol M:Shop.Counter.Update(System.Int32)
Arguments
event"Value"
Refusesmember-exists; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 private int _value;
6
7 public int Value => _value;
8
9 public void Update(int value)
10 {
11 _value = value;
12 }
13 }
14 }

#no-body

Refuses an abstract method, which has no body to raise the event from

refusal
Targetsymbol M:Shop.Shape.Resize(System.Double)
Arguments
event"Resized"
Refusesno-body; every file is left unchanged
Shape.csinput
1 namespace Shop
2 {
3 public abstract class Shape
4 {
5 public abstract void Resize(double factor);
6 }
7 }

#not-void

Refuses a method that returns a value

refusal
Targetsymbol M:Shop.Counter.Next
Arguments
event"Advanced"
Refusesnot-void; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 private int _value;
6
7 public int Next()
8 {
9 return ++_value;
10 }
11 }
12 }

#nullable-and-generic

In a nullable context the event is declared nullable, and a type parameter of the class is passed through

success
Targetsymbol M:Shop.Store`1.Save(`0,System.String)
Arguments
event"Saved"
Projectnullable enable
Store.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Store<T>
77 {
88 private readonly Dictionary<string, T> _items = new Dictionary<string, T>();
99
10+ public event Action<T, string?>? Saved;
11+
1012 public void Save(T item, string? key)
1113 {
1214 _items[key ?? "default"] = item;
15+ Saved?.Invoke(item, key);
1316 }
1417 }
1518 }

#ref-parameter

Refuses a method with a ref or out parameter, which an Action cannot take

refusal
Targetsymbol M:Shop.Counter.Swap(System.Int32@)
Arguments
event"Swapped"
Refusesref-parameter; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 private int _value;
6
7 public void Swap(ref int other)
8 {
9 var old = _value;
10 _value = other;
11 other = old;
12 }
13 }
14 }

#single-parameter

Declares an Action event taking the method's parameter just before the method, raises it at the end of the method, and imports System

success
Targetsymbol M:Shop.Counter.Update(System.Int32)
Arguments
event"Updated"
Counter.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Counter
46 {
57 private int _value;
68
79 public int Value => _value;
810
11+ public event Action<int> Updated;
12+
913 public void Update(int value)
1014 {
1115 _value = value;
16+ Updated?.Invoke(value);
1217 }
1318 }
1419 }

#static-expression-bodied

A static method raises a static event; an expression body becomes a block

success
Targetsymbol M:Shop.Audit.Record(System.String)
Arguments
event"Recorded"
Audit.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public static class Audit
77 {
88 private static readonly List<string> Entries = new List<string>();
99
10− public static void Record(string entry) => Entries.Add(entry);
10+ public static event Action<string> Recorded;
11+
12+ public static void Record(string entry)
13+ {
14+ Entries.Add(entry);
15+ Recorded?.Invoke(entry);
16+ }
1117 }
1218 }