RefactorMCP catalog

Move Instance Method

Moves an instance method onto the type of one of its class's fields or properties, or of one of its own parameters. That field, property or parameter (the "via") becomes this in the method's new home. Or, with into, moves it the other way: into the class that holds its class in a field or property (the "holder"), which it then reaches the rest of its old class through.

Arguments

ArgumentMeaning
target.symbolthe method, such as M:Shop.Customer.Label
viathe field, property or parameter to move through, such as _address
toinstead of via: the target type's name; the one field, property or parameter of that type is used
intoinstead of via or to: the class that holds the method's class in a field or property, such as Customer; no stub is left
stubtrue (the default) leaves a delegating method behind; false removes it and updates every call

Composite recipes use the via form, for example { "refactoring": "move-instance-method", "target": { "symbol": "M:Shop.Customer.FormatAddress" }, "arguments": { "via": "_address" } }, and the into form to move a method back towards the class that holds its own, as in { "refactoring": "move-instance-method", "target": { "symbol": "M:Shop.Address.Format" }, "arguments": { "into": "Customer" } }.

Precondition

  • The method is an ordinary instance method, not virtual, abstract, an override or an interface implementation, and does not call through base or call itself.
  • The via exists, and with to, exactly one field, property or parameter has the target type. The via's type is a class or struct the solution declares, is not the method's own class, and is not a constructed generic type.
  • The method does not assign the via.
  • The target has no member of the same name, other than methods with different parameters.
  • The method uses no protected member of its class.
  • When the method still needs its class's instance, the target's project can see the class's project.

Without a stub, additionally:

  • Every caller can access a via field or property.
  • The method is only called, not used as a method group or through null-conditional access.
  • A call whose receiver would be evaluated twice (because the instance is also passed) or dropped (because a parameter becomes the receiver) is made on a simple name, not an arbitrary expression.
  • No call passes null for a via parameter.

With into:

  • The member is an instance member. The into class, declared in the solution, has exactly one field or property whose type is the member's class: the holder.
  • The holder is an instance field, or a get-only auto-property, that creates its object in its initializer and is never assigned. Each instance of the holding class then has exactly one instance of the member's class, from the start, whose state the member can take over.
  • Every use of the member goes through the holder: _address.Street, this._address.Street or customer.Address.Street. A use inside its own class, or through any other instance, would have no holder to go through.
  • The holding class has no method of the same name and parameters.
  • The method uses no protected member of its class and does not call through base.

Transformation

  • The method is appended to the target type. Uses of the via become this: _address.Street becomes Street, or this.Street where a local of that name hides it. A via parameter is dropped from the parameter list.
  • When the method uses other members of its class, it takes the instance as a new first parameter named after the class in camel case (Customer customer), and those uses go through it: Name becomes customer.Name, this becomes customer. Static members of the class are qualified by it.
  • Private members of the class the moved method still uses become internal; a private method moves as internal.
  • Types the method names are imported into the target file as needed, and the source file drops usings only the method needed.
  • With a stub, the original method keeps its signature and documentation comment and calls the moved one: return _address.Label(this);. Ordinary comments above the method move with it.
  • Without a stub, the original is removed and calls are rewritten: customer.Label() becomes customer.Address.Label(customer), and a call warehouse.Track(parcel, "x") through a via parameter becomes parcel.Track("x").
  • With into, the method keeps its accessibility and is appended to the holding class, and the original is removed. Uses of the rest of its old class go through the holder: Street and this.Street become Address.Street, and this becomes Address; private members it uses there become internal. A call of the method itself stays as it is. Calls through the holder become direct: Address.Format() becomes Format() and customer.Address.Format() becomes customer.Format().

Preserved

  • The behaviour of every call.
  • The method's body, comments and documentation.

Limitations

  • Private members are raised to internal rather than exposed through a narrower mechanism.
  • A method whose target type is a constructed generic (Repository<Order>) is refused rather than rewritten against the type's parameters.
  • The method is always appended at the end of the target type.

Error codes

CodeMeaning
method-is-staticthe method is static; Move Static Method moves it
via-not-foundno field, property or parameter has the via's name
no-reference-to-targetno field, property or parameter has the to type
ambiguous-targetseveral fields, properties or parameters have the to type
target-not-in-sourcethe via's type is not declared in the solution
target-not-classthe via's type is not a class or struct
generic-targetthe via's type is a constructed generic type
same-typethe via's type is the method's own class
member-existsthe target already has a member with that name and parameters
polymorphic-methodthe method is virtual, abstract, an override or implements an interface
uses-basethe method calls through base
recursive-methodthe method calls itself
uses-protected-memberthe method uses a protected member of its class
via-assignedthe method assigns the via
via-not-accessiblewithout a stub, a caller cannot access the via
method-groupwithout a stub, the method is used as a method group
conditional-accesswithout a stub, a call uses null-conditional access
complex-receiverwithout a stub, a call's receiver is not a simple name
null-argumentwithout a stub, a call passes null for the via parameter
name-conflictthe new instance parameter's name is already taken
target-cannot-see-sourcethe target's project cannot see the method's class
holder-not-foundwith into, the class has no field or property of the member's class
several-holderswith into, the class has several fields or properties of the member's class
holder-not-createdwith into, the holder is static, does not create its object in its initializer, or is assigned
used-outside-holderwith into, a use of the member does not go through the holder

Cases

·

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

#across-projects

A method that needs nothing of its class moves into a type in a project its own project references

success
Targetsymbol M:App.Customer.Town
Arguments
via"_address"
ProjectsCore; App → Core
App/Customer.csmodified
11 using Core;
22
33 namespace App
44 {
55 public class Customer
66 {
77 private readonly Address _address = new Address();
88
9− public string Town() => _address.City.ToUpperInvariant();
9+ public string Town() => _address.Town();
1010 }
1111 }
Core/Address.csmodified
11 namespace Core
22 {
33 public class Address
44 {
55 public string City { get; set; }
6+
7+ public string Town() => City.ToUpperInvariant();
68 }
79 }

#across-projects-rejected

Refuses when the target's project cannot see the class whose instance the moved method would take

refusal
Targetsymbol M:App.Customer.Label
Arguments
via"_address"
ProjectsCore; App → Core
Refusestarget-cannot-see-source; every file is left unchanged
App/Customer.csinput
1 using Core;
2
3 namespace App
4 {
5 public class Customer
6 {
7 private readonly Address _address = new Address();
8
9 public string Name { get; set; }
10
11 public string Label() => Name + ", " + _address.Town;
12 }
13 }
Core/Address.csinput
1 namespace Core
2 {
3 public class Address
4 {
5 public string Town { get; set; }
6 }
7 }

#ambiguous-target-rejected

Refuses a target type reachable through two fields; via names the one to use

refusal
Targetsymbol M:Shop.Customer.Towns
Arguments
to"Address"
Refusesambiguous-target; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _home = new Address();
6 private readonly Address _work = new Address();
7
8 public string Towns() => _home.Town + _work.Town;
9 }
10
11 public class Address
12 {
13 public string Town { get; set; }
14 }
15 }

#inherited-members

Members the method uses from its class's base class are reached through the source instance like its own

success
Targetsymbol M:Shop.Customer.Greeting
Arguments
via"_address"
Customer.csmodified
11 namespace Shop
22 {
33 public class Party
44 {
55 public string Name { get; set; }
66 }
77
88 public class Customer : Party
99 {
1010 private readonly Address _address = new Address();
1111
12− public string Greeting() => "Dear " + Name + " of " + _address.Town;
12+ public string Greeting() => _address.Greeting(this);
1313 }
1414
1515 public class Address
1616 {
1717 public string Town { get; set; }
18+
19+ public string Greeting(Customer customer) => "Dear " + customer.Name + " of " + Town;
1820 }
1921 }

#into-holder

Into the class holding the method's class: uses of the old class's members go through the holder, calls through the holder become direct, and a private member it uses becomes internal

success
Targetsymbol M:Shop.Address.Format
Arguments
into"Customer"
Address.csmodified
11 namespace Shop
22 {
33 public class Address
44 {
5− private string _country = "UK";
5+ internal string _country = "UK";
66
77 public string Street { get; set; }
88
99 public string City { get; set; }
10−
11− // One line, for labels.
12− public string Format()
13− {
14− return Street + ", " + City + ", " + this._country;
15− }
1610 }
1711 }
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 public Address Address { get; } = new Address();
66
7− public string Label() => Address.Format();
7+ public string Label() => Format();
8+
9+ // One line, for labels.
10+ public string Format()
11+ {
12+ return Address.Street + ", " + Address.City + ", " + Address._country;
13+ }
814 }
915 }
Labels.csmodified
11 namespace Shop
22 {
33 public class Labels
44 {
5− public string For(Customer customer) => customer.Address.Format();
5+ public string For(Customer customer) => customer.Format();
66 }
77 }

#into-member-exists-rejected

Refuses to move into a holding class that already has a method of the same name and parameters

refusal
Targetsymbol M:Shop.Address.Format
Arguments
into"Customer"
Refusesmember-exists; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6
7 public string Format() => Street;
8 }
9
10 public class Customer
11 {
12 private readonly Address _address = new Address();
13
14 public string Format() => "Customer at " + _address.Format();
15 }
16 }

#into-used-outside-holder-rejected

Refuses to move into the holding class a method its own class still calls, since that call has no holder to go through

refusal
Targetsymbol M:Shop.Address.Format
Arguments
into"Customer"
Refusesused-outside-holder; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6
7 public string Format() => Street;
8
9 public string Shout() => Format().ToUpper();
10 }
11
12 public class Customer
13 {
14 private readonly Address _address = new Address();
15
16 public string Label() => _address.Format();
17 }
18 }

#member-exists-rejected

Refuses when the target already has a method with the moved method's name and parameters

refusal
Targetsymbol M:Shop.Customer.Label
Arguments
via"_address"
Refusesmember-exists; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public string Label() => _address.Town;
8 }
9
10 public class Address
11 {
12 public string Town { get; set; }
13
14 public string Label() => Town;
15 }
16 }

#no-reference-to-target-rejected

Refuses a target type the method cannot reach through a field, property or parameter

refusal
Targetsymbol M:Shop.Customer.Label
Arguments
to"Address"
Refusesno-reference-to-target; 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 Label() => Name;
8 }
9
10 public class Address
11 {
12 }
13 }

#overload

Only the chosen overload moves; the other stays and still calls through the stub

success
Targetsymbol M:Shop.Customer.Label(System.String)
Arguments
via"_address"
Address.csmodified
11 namespace Shop
22 {
33 public class Address
44 {
55 public string Town { get; set; }
6+
7+ public string Label(string prefix) => prefix + Town;
68 }
79 }
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 private readonly Address _address = new Address();
66
77 public string Label() => Label("");
88
9− public string Label(string prefix) => prefix + _address.Town;
9+ public string Label(string prefix) => _address.Label(prefix);
1010 }
1111 }

#polymorphic-rejected

Refuses an override, whose callers rely on virtual dispatch

refusal
Targetsymbol M:Shop.Customer.ToString
Arguments
via"_address"
Refusespolymorphic-method; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public override string ToString() => _address.Town;
8 }
9
10 public class Address
11 {
12 public string Town { get; set; }
13 }
14 }

#raises-accessibility

Private members the moved method still uses through the instance become internal, and a private method moves as internal

success
Targetsymbol M:Shop.Customer.Discounted(System.Decimal)
Arguments
via"_account"
Account.csmodified
11 namespace Shop
22 {
33 public class Account
44 {
55 public decimal Rebate { get; set; }
6+
7+ internal decimal Discounted(Customer customer, decimal amount) => amount * (1 - customer._loyalty - Rebate);
68 }
79 }
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 private readonly Account _account = new Account();
6− private decimal _loyalty = 0.05m;
6+ internal decimal _loyalty = 0.05m;
77
88 public decimal Pay(decimal amount) => Discounted(amount);
99
10− private decimal Discounted(decimal amount) => amount * (1 - _loyalty - _account.Rebate);
10+ private decimal Discounted(decimal amount) => _account.Discounted(this, amount);
1111 }
1212 }

#static-method-rejected

Refuses a static method, which Move Static Method moves

refusal
Targetsymbol M:Shop.Customer.Format(System.String)
Arguments
to"Address"
Refusesmethod-is-static; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public static string Format(string text) => text.Trim();
6 }
7
8 public class Address
9 {
10 }
11 }

#target-in-other-namespace

Moving into a type in another namespace imports what the method uses, and the method's static helpers are qualified by their class

success
Targetsymbol M:Shop.Customer.Summary
Arguments
via"_address"
Customer.csmodified
11 using System.Collections.Generic;
2−using System.Linq;
32 using Shop.Places;
43
54 namespace Shop
65 {
76 public class Customer
87 {
98 private readonly Address _address = new Address();
109
1110 public List<Order> Orders { get; } = new List<Order>();
1211
13− public string Summary() => Format(_address.Town) + ": " + Orders.Count(o => o.Total > 0);
12+ public string Summary() => _address.Summary(this);
1413
15− private static string Format(string text) => text.Trim();
14+ internal static string Format(string text) => text.Trim();
1615 }
1716
1817 public class Order
1918 {
2019 public decimal Total { get; set; }
2120 }
2221 }
Places/Address.csmodified
1+using System.Linq;
2+
13 namespace Shop.Places
24 {
35 public class Address
46 {
57 public string Town { get; set; }
8+
9+ public string Summary(Customer customer) => Customer.Format(Town) + ": " + customer.Orders.Count(o => o.Total > 0);
610 }
711 }

#target-not-in-source-rejected

Refuses to move into a type the solution does not declare

refusal
Targetsymbol M:Shop.Customer.Shout
Arguments
via"_name"
Refusestarget-not-in-source; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private string _name = "";
6
7 public string Shout() => _name.ToUpperInvariant();
8 }
9 }

#to-target-type

Naming the target type finds the one field of that type to move through

success
Targetsymbol M:Shop.Customer.Where
Arguments
to"Address"
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 private readonly Address _home = new Address();
66
77 public string Name { get; set; }
88
9− public string Where() => _home.Town.ToUpperInvariant();
9+ public string Where() => _home.Where();
1010 }
1111
1212 public class Address
1313 {
1414 public string Town { get; set; }
15+
16+ public string Where() => Town.ToUpperInvariant();
1517 }
1618 }

#via-assigned-rejected

Refuses a method that assigns the field it would move through

refusal
Targetsymbol M:Shop.Customer.Reset
Arguments
via"_address"
Refusesvia-assigned; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private Address _address = new Address();
6
7 public void Reset() => _address = new Address();
8 }
9
10 public class Address
11 {
12 }
13 }

#via-field-with-stub

Moves a method onto the type of a field; the source instance is passed for the members it still needs, and a delegating stub stays behind

success
Targetsymbol M:Shop.Customer.Label
Arguments
via"_address"
Address.csmodified
11 namespace Shop
22 {
33 public class Address
44 {
55 public string Street { get; set; }
66
77 public string Town { get; set; }
8+
9+ /// <summary>The customer as written on an envelope.</summary>
10+ // Street and town go on separate lines.
11+ public string Label(Customer customer)
12+ {
13+ return customer.Name + "\n" + Street + "\n" + Town;
14+ }
815 }
916 }
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 private readonly Address _address = new Address();
66
77 public string Name { get; set; }
88
99 /// <summary>The customer as written on an envelope.</summary>
10− // Street and town go on separate lines.
1110 public string Label()
1211 {
13− return Name + "\n" + _address.Street + "\n" + this._address.Town;
12+ return _address.Label(this);
1413 }
1514 }
1615 }

#via-not-accessible-rejected

Refuses to remove the method when a caller could not reach the private field it would call through

refusal
Targetsymbol M:Shop.Customer.Where
Arguments
via"_address"
stubfalse
Refusesvia-not-accessible; every file is left unchanged
Checkout.csinput
1 namespace Shop
2 {
3 public class Checkout
4 {
5 public string Where(Customer customer) => customer.Where();
6 }
7 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public string Where() => _address.Town;
8 }
9
10 public class Address
11 {
12 public string Town { get; set; }
13 }
14 }

#via-parameter

Moves a method onto the type of one of its parameters, which it no longer takes; it uses nothing else of its class, so no instance is passed

success
Targetsymbol M:Shop.Warehouse.Track(Shop.Parcel,System.String)
Arguments
via"parcel"
Parcel.csmodified
11 namespace Shop
22 {
33 public class Parcel
44 {
55 public string Code { get; set; }
66
77 public decimal Weight { get; set; }
8+
9+ public string Track(string prefix)
10+ {
11+ return prefix + Code + " (" + Weight + "kg)";
12+ }
813 }
914 }
Warehouse.csmodified
11 namespace Shop
22 {
33 public class Warehouse
44 {
55 public string Track(Parcel parcel, string prefix)
66 {
7− return prefix + parcel.Code + " (" + parcel.Weight + "kg)";
7+ return parcel.Track(prefix);
88 }
99 }
1010 }

#via-parameter-without-stub

Without a stub, each call's argument for the parameter becomes the receiver of the call

success
Targetsymbol M:Shop.Warehouse.Track(Shop.Parcel,System.String)
Arguments
via"parcel"
stubfalse
Depot.csmodified
11 namespace Shop
22 {
33 public class Depot
44 {
55 public Warehouse Warehouse { get; } = new Warehouse();
66
7− public string Label(Parcel parcel) => Warehouse.Track(parcel, "Depot: ");
7+ public string Label(Parcel parcel) => parcel.Track("Depot: ");
88 }
99 }
Parcel.csmodified
11 namespace Shop
22 {
33 public class Parcel
44 {
55 public string Code { get; set; }
6+
7+ public string Track(string prefix) => prefix + Code;
68 }
79 }
Warehouse.csmodified
11 namespace Shop
22 {
33 public class Warehouse
44 {
5− public string Track(Parcel parcel, string prefix) => prefix + parcel.Code;
6−
7− public string TrackAll(Parcel first, Parcel second) => Track(first, "1: ") + Track(second, "2: ");
5+ public string TrackAll(Parcel first, Parcel second) => first.Track("1: ") + second.Track("2: ");
86 }
97 }

#via-property-without-stub

Without a stub, the method is removed and every call goes through the property, passing the instance it was called on

success
Targetsymbol M:Shop.Customer.Label
Arguments
via"Address"
stubfalse
Address.csmodified
11 namespace Shop
22 {
33 public class Address
44 {
55 public string Town { get; set; }
6+
7+ public string Label(Customer customer) => customer.Name + ", " + Town;
68 }
79 }
Checkout.csmodified
11 namespace Shop
22 {
33 public class Checkout
44 {
5− public string Print(Customer customer) => customer.Label();
5+ public string Print(Customer customer) => customer.Address.Label(customer);
66 }
77 }
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 public Address Address { get; } = new Address();
66
77 public string Name { get; set; }
88
9− public string Label() => Name + ", " + Address.Town;
10−
11− public string Envelope() => "To: " + Label();
9+ public string Envelope() => "To: " + Address.Label(this);
1210 }
1311 }