RefactorMCP catalog

Add Delegating Member

Adds to a class a member that forwards to the member of the same name of one of its fields, or of its base class. Replace Inheritance with Delegation uses it to give a class, while it still derives from its base class, its own members for the inherited ones other code uses, so the class can later stop deriving without its callers noticing.

Arguments

ArgumentRequiredMeaning
memberyesThe member to forward to: its name, this for an indexer, or its documentation comment id (M:Shop.Printer.Print(System.String)) when overloads share the name
viayesThe name of the field to forward through, or base for the base class

The target is the class, by symbol: "target": { "symbol": "T:Shop.Stack" }.

Precondition

  • The target is a class or struct; with base, a class deriving from a class other than object. With a field name, the class declares that field.
  • The field's type, or the base class and the classes it derives from, has an instance member of that name the class can reach through the field or base, and exactly one unless it is named by id.
  • The member is a method, property, indexer or field. Events, static members, generic methods, methods with ref, out, in, params or optional parameters, and members returning by reference are refused.
  • The class does not already declare a member with that name, other than a method overload with different parameter types.
  • No existing reference changes meaning, other than in the ways below, and the result compiles.

Transformation

  • The new member has the forwarded member's name, type, parameters and accessibility. A method becomes an expression-bodied method (public string Greeting(string salutation) => _customer.Greeting(salutation);). A property or indexer becomes a property or indexer whose getter reads the member and, when the forwarded member has a setter as accessible as itself, whose setter writes it, with the accessors on one line. A field becomes a property, get-only when the field is readonly.
  • It is placed after the class's fields and the members right after them that already forward through the same field or base, or first in the class when there are none, with a blank line on each side.
  • When it hides an inherited member, as a member forwarding to base always does, it is declared new.
  • Inside the class, uses of an inherited member the new one hides, written bare or with this, are written with base (this[Count - 1] becomes this[base.Count - 1] when forwarding Count), so they keep reaching the inherited member.
  • Elsewhere, uses through the class of the member forwarded to base now reach the new member, which reaches the same member.

Preserved

  • What every existing reference reaches when it runs.
  • Comments and blank lines around the neighbouring members.

Limitations

  • Implicit uses, such as a collection initializer calling an Add the new member hides, are not checked.
  • The accessors of a forwarding property or indexer are written on one line.

Error codes

CodeMeaning
not-a-classthe target is not a class or struct
field-not-foundthe class has no field with the name via gives
no-base-classvia is base and the class derives only from object
member-not-foundthe field's type or the base class has no such member the class can reach
ambiguous-memberseveral members share the name; name one by its documentation comment id
unsupported-memberthe member is an event, static, generic, has ref, out, in, params or optional parameters, or returns by reference
name-conflictthe class already declares a member of that name
changes-meaninga reference outside the class, or to a member the class declares, would reach the new member instead
breaks-compilationthe result would not compile, for example because the new member would expose a less accessible type

Cases

·

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

#after-existing-delegating-members

The new member goes after the members that already forward to the same field, and comments on the neighbouring members stay with them

success
Targetsymbol T:Shop.Order
Arguments
member"Greeting"
via"_customer"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 // Who placed the order.
66 private readonly Customer _customer = new Customer();
77
88 // Forwarded so callers need not reach the customer.
99 public string Name => _customer.Name;
1010
11+ public string Greeting(string salutation) => _customer.Greeting(salutation);
12+
1113 /// <summary>The order's total.</summary>
1214 public decimal Total { get; set; }
1315 }
1416 }
Customer.csunchanged
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name { get; set; } = "";
66
77 public string Greeting(string salutation) => salutation + " " + Name;
88 }
99 }

#after-forwarding-to-base

A second member forwarding to the base class goes after the first, and callers elsewhere that used the inherited indexer reach the new one

success
Targetsymbol T:Shop.Stack
Arguments
member"this"
via"base"
Stack.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 // Last in, first out.
66 public class Stack : List<int>
77 {
88 private readonly List<int> _list = new List<int>();
99
1010 public new int Count => base.Count;
1111
12+ public new int this[int index] { get => base[index]; set => base[index] = value; }
13+
1214 public void Push(int value) => Add(value);
1315
1416 public int Pop()
1517 {
16− var top = this[base.Count - 1];
18+ var top = base[base.Count - 1];
1719 RemoveAt(base.Count - 1);
1820 return top;
1921 }
2022 }
2123 }
Client.csunchanged
11 namespace Shop
22 {
33 public class Client
44 {
55 public int Run()
66 {
77 var stack = new Stack();
88 stack.Push(1);
99 stack[0] = 5;
1010 return stack.Pop() + stack.Count + stack[0];
1111 }
1212 }
1313 }

#ambiguous-member-rejected

Refuses a name several overloads share; the member must be named by its id

refusal
Targetsymbol T:Shop.Report
Arguments
member"Print"
via"_printer"
Refusesambiguous-member; every file is left unchanged
Printer.csinput
1 namespace Shop
2 {
3 public class Printer
4 {
5 public void Print(string text) { }
6
7 public void Print(int number) { }
8 }
9 }
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 private readonly Printer _printer = new Printer();
6 }
7 }

#breaks-compilation-rejected

Refuses when the forwarding member would not compile: a public method of a public class cannot return an internal type

refusal
Targetsymbol T:Shop.Car
Arguments
member"Spare"
via"_engine"
Refusesbreaks-compilation; every file is left unchanged
Car.csinput
1 namespace Shop
2 {
3 internal class Part
4 {
5 }
6
7 internal class Engine
8 {
9 public Part Spare() => new Part();
10 }
11
12 public class Car
13 {
14 private readonly Engine _engine = new Engine();
15
16 public bool HasEngine() => _engine != null;
17 }
18 }

#changes-meaning-rejected

Refuses when the new member would hide an inherited member other code uses, which would then reach the field instead

refusal
Targetsymbol T:Shop.Order
Arguments
member"Name"
via"_customer"
Refuseschanges-meaning mentioning “Order.cs(17,”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6
7 public string Greeting(string salutation) => salutation + " " + Name;
8 }
9 }
Order.csinput
1 namespace Shop
2 {
3 public class Document
4 {
5 public string Name { get; set; } = "";
6 }
7
8 public class Order : Document
9 {
10 private readonly Customer _customer = new Customer();
11
12 public string Label() => Name + " for " + _customer.Name;
13 }
14
15 public class Archive
16 {
17 public string Title(Order order) => order.Name;
18 }
19 }

#field-as-property

A readonly field of the delegate is forwarded by a get-only property

success
Targetsymbol T:Shop.OrderLine
Arguments
member"Sku"
via"_product"
OrderLine.csmodified
11 namespace Shop
22 {
33 public class OrderLine
44 {
55 private readonly Product _product = new Product();
66 private int _quantity = 1;
77
8+ public string Sku => _product.Sku;
9+
810 public decimal Total() => _product.Price * _quantity;
911 }
1012 }
Product.csunchanged
11 namespace Shop
22 {
33 public class Product
44 {
55 public readonly string Sku = "A-1";
66 public decimal Price;
77 }
88 }

#field-not-found-rejected

Refuses when the class has no field of the given name

refusal
Targetsymbol T:Shop.Order
Arguments
member"Name"
via"_buyer"
Refusesfield-not-found mentioning “_buyer”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6
7 public string Greeting(string salutation) => salutation + " " + Name;
8 }
9 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly Customer _customer = new Customer();
6
7 public decimal Total { get; set; }
8 }
9 }

#generic-field-type

Forwarding to a field of a constructed generic type writes the member with the type arguments substituted

success
Targetsymbol T:Shop.Tally
Arguments
member"ContainsKey"
via"_counts"
Tally.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Tally
66 {
77 private readonly Dictionary<string, int> _counts = new Dictionary<string, int>();
88
9+ public bool ContainsKey(string key) => _counts.ContainsKey(key);
10+
911 public void Count(string word) => _counts[word] = _counts.GetValueOrDefault(word) + 1;
1012 }
1113 }

#indexer-via-base

An indexer is named this; the forwarding indexer keeps its setter, and the class's own element accesses use base

success
Targetsymbol T:Shop.Stack
Arguments
member"this"
via"base"
Stack.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Stack : List<int>
66 {
7+ public new int this[int index] { get => base[index]; set => base[index] = value; }
8+
79 public void Push(int value) => Add(value);
810
911 public int Pop()
1012 {
11− var top = this[Count - 1];
13+ var top = base[Count - 1];
1214 RemoveAt(this.Count - 1);
1315 return top;
1416 }
1517 }
1618 }

#member-not-found-rejected

Refuses a member the field's type does not have

refusal
Targetsymbol T:Shop.Order
Arguments
member"Surname"
via"_customer"
Refusesmember-not-found mentioning “Surname”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6
7 public string Greeting(string salutation) => salutation + " " + Name;
8 }
9 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly Customer _customer = new Customer();
6
7 public decimal Total { get; set; }
8 }
9 }

#member-of-grandbase

Forwarding to the base reaches a member the base class inherited itself

success
Targetsymbol T:Staff.Manager
Arguments
member"Name"
via"base"
People.csmodified
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66 }
77
88 public class Employee : Person
99 {
1010 public decimal Salary { get; set; }
1111 }
1212
1313 public class Manager : Employee
1414 {
15− public string Title() => "Manager " + Name;
15+ public new string Name { get => base.Name; set => base.Name = value; }
16+
17+ public string Title() => "Manager " + base.Name;
1618 }
1719 }

#name-conflict-rejected

Refuses when the class already declares a member of that name

refusal
Targetsymbol T:Shop.Order
Arguments
member"Greeting"
via"_customer"
Refusesname-conflict mentioning “Greeting”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6
7 public string Greeting(string salutation) => salutation + " " + Name;
8 }
9 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly Customer _customer = new Customer();
6
7 public string Greeting(string salutation) => salutation + ", your order";
8 }
9 }

#no-base-class-rejected

Refuses to forward to the base class of a class that derives only from object

refusal
Targetsymbol T:Shop.Order
Arguments
member"Name"
via"base"
Refusesno-base-class; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6
7 public string Greeting(string salutation) => salutation + " " + Name;
8 }
9 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly Customer _customer = new Customer();
6
7 public decimal Total { get; set; }
8 }
9 }

#not-a-class-rejected

Refuses an interface, which cannot hold a field or forward to a base class

refusal
Targetsymbol T:Shop.IOrder
Arguments
member"Name"
via"base"
Refusesnot-a-class; every file is left unchanged
IOrder.csinput
1 namespace Shop
2 {
3 public interface IOrder
4 {
5 decimal Total { get; }
6 }
7 }

#nullable

Nullable annotations on the forwarded member's type and parameters are kept

success
Targetsymbol T:Shop.Order
Arguments
member"Find"
via"_book"
Order.csmodified
11 #nullable enable
22
33 namespace Shop
44 {
55 public class Order
66 {
77 private readonly AddressBook _book = new AddressBook();
8+
9+ public string? Find(string? prefix) => _book.Find(prefix);
810 }
911 }
AddressBook.csunchanged
11 #nullable enable
22
33 namespace Shop
44 {
55 public class AddressBook
66 {
77 public string? Find(string? prefix) => prefix;
88 }
99 }

#overload-by-id

An overloaded member is named by its documentation comment id, and only that overload is forwarded

success
Targetsymbol T:Shop.Report
Arguments
member"M:Shop.Printer.Print(System.String)"
via"_printer"
Report.csmodified
11 namespace Shop
22 {
33 public class Report
44 {
55 private readonly Printer _printer = new Printer();
6+
7+ public void Print(string text) => _printer.Print(text);
68 }
79 }
Printer.csunchanged
11 namespace Shop
22 {
33 public class Printer
44 {
55 public void Print(string text) { }
66
77 public void Print(int number) { }
88 }
99 }

#property-with-setter

A property with a setter the class can reach is forwarded with both accessors

success
Targetsymbol T:Shop.Order
Arguments
member"Name"
via"_customer"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 private readonly Customer _customer = new Customer();
66
7+ public string Name { get => _customer.Name; set => _customer.Name = value; }
8+
79 public decimal Total { get; set; }
810 }
911 }
Customer.csunchanged
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name { get; set; } = "";
66
77 public string Greeting(string salutation) => salutation + " " + Name;
88 }
99 }

#unsupported-member-rejected

Refuses an event, which a forwarding member cannot reproduce

refusal
Targetsymbol T:Shop.Order
Arguments
member"Changed"
via"_customer"
Refusesunsupported-member; every file is left unchanged
Customer.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Customer
6 {
7 public event EventHandler Changed;
8
9 public void Touch() => Changed?.Invoke(this, EventArgs.Empty);
10 }
11 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly Customer _customer = new Customer();
6
7 public decimal Total { get; set; }
8 }
9 }

#via-base

A member forwarding to the base class hides the inherited one; the class's own uses of the inherited member say base so they keep reaching it, and callers elsewhere reach the new member

success
Targetsymbol T:Shop.Stack
Arguments
member"Count"
via"base"
Stack.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Stack : List<int>
66 {
7+ public new int Count => base.Count;
8+
79 public void Push(int value) => Add(value);
810
911 public int Pop()
1012 {
11− var top = this[Count - 1];
12− RemoveAt(this.Count - 1);
13+ var top = this[base.Count - 1];
14+ RemoveAt(base.Count - 1);
1315 return top;
1416 }
1517 }
1618 }
Client.csunchanged
11 namespace Shop
22 {
33 public class Client
44 {
55 public int Run()
66 {
77 var stack = new Stack();
88 stack.Push(1);
99 return stack.Pop() + stack.Count;
1010 }
1111 }
1212 }

#via-field

Adds a method that forwards to the same method of a field, after the fields

success
Targetsymbol T:Shop.Order
Arguments
member"Greeting"
via"_customer"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 private readonly Customer _customer = new Customer();
66
7+ public string Greeting(string salutation) => _customer.Greeting(salutation);
8+
79 public decimal Total { get; set; }
810 }
911 }
Customer.csunchanged
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name { get; set; } = "";
66
77 public string Greeting(string salutation) => salutation + " " + Name;
88 }
99 }