RefactorMCP catalog

Inline Class

Moves every member of a class that is not pulling its weight into the one class that holds an instance of it, and deletes it. The reverse of Extract Class.

Recipe

  1. Move each member into the holding class with the into form of the move primitives, which rewrites its uses through the holder to be direct. A method moves before the members it uses, since until then its uses of them are inside the class being inlined, with no holder to go through; it reaches them through the holder meanwhile. { "refactoring": "move-instance-method", "target": { "symbol": "M:Shop.Address.Format" }, "arguments": { "into": "Customer" } }, { "refactoring": "move-property", "target": { "symbol": "P:Shop.Address.Street" }, "arguments": { "into": "Customer" } }, { "refactoring": "move-field", "target": { "symbol": "F:Shop.Address._country" }, "arguments": { "into": "Customer" } }.
  2. Change Accessibility back to private on each private member that a moved method made internal so it could reach it through the holder: { "refactoring": "change-accessibility", "target": { "symbol": "F:Shop.Customer._country" }, "arguments": { "accessibility": "private" } }.
  3. Safe Delete Member on the holder, now unused: { "refactoring": "safe-delete-member", "target": { "symbol": "F:Shop.Customer._address" } }.
  4. Safe Delete Type on the class: { "refactoring": "safe-delete-type", "target": { "symbol": "T:Shop.Address" } }.

The moves place fields after the holding class's last field and other members at its end in the order they move, so the result matches the dedicated implementation when the members are moved in their declared order. A class whose methods follow the members they use, as is usual, ends up with those methods first.

Target

The class to inline, by symbol: "target": { "symbol": "T:Shop.Address" }.

Precondition

  • The class is a plain class: not static, abstract, generic or partial; no base class other than object and no interfaces; no constructor, finalizer, static member or nested type.
  • Exactly one instance field or get-only auto-property of another class (the holder) has the class's type, and it creates the object in its initializer with new Address() or new(), without arguments or an object initializer. So each instance of the holding class has exactly one, from the start, and it is never replaced.
  • Nothing else names the class: no parameter, local, return type, base list, typeof, nameof or other new.
  • Every use of the holder reaches a member of the class, as in _address.Street; it is not passed on, compared, assigned or reached through ?..
  • No member of the holding class, or of a class it derives from, has the name of a member of the class.
  • The result compiles.

Transformation

  • Each member of the class moves into the holding class, with its comments and modifiers: fields after the holding class's last field, other members at its end, in their order.
  • Each use through the holder becomes direct: _address.Street becomes Street, or this.Street where a local or parameter of that name hides it; this._address.Street becomes this.Street; and customer.Address.Street in another class becomes customer.Street.
  • The holder is removed, and the class is deleted, with its file when nothing else is left in it.
  • The holding class's file gains the usings the moved members need.

Preserved

  • The behaviour of every use: each holding instance had exactly one instance of the class, whose state now lives in the holding instance itself.
  • Initializers of the moved fields and properties, which run when the holding class is constructed, as the holder's new did.

Limitations

  • Members keep their accessibility, so a public member of the class becomes a public member of the holding class; narrow it with Change Accessibility.
  • A class whose namespace holds nothing else leaves using directives for that namespace behind, which the compile check refuses.
  • A class that is constructed with arguments or set up in a constructor is refused rather than having its set-up merged into the holding class.

Error codes

CodeMeaning
unsupported-classthe class is not a plain class that its members can leave
type-referencedcode other than the holder names the class
no-holderno field or property holds an instance of the class
several-holdersseveral fields or properties hold an instance of the class
holder-not-createdthe holder is static, or does not create the object in its initializer
member-existsthe holding class already has a member of the same name
holder-escapesa use of the holder does something other than reach a member of the class

Cases

·

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

#fields-and-methods

Fields, properties and methods move into the class holding the inlined one, uses through the holding field become direct, and the class and its file are deleted

success
Targetsymbol T:Shop.Address
Address.csdeleted
1−namespace Shop
2−{
3− /// <summary>Where a customer lives.</summary>
4− public class Address
5− {
6− private readonly string _country = "UK";
7−
8− public string Street { get; set; }
9−
10− public string City { get; set; }
11−
12− // One line, for labels.
13− public string Format() => Street + ", " + City + ", " + _country;
14− }
15−}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private readonly Address _address = new Address();
5+ private readonly string _country = "UK";
66
77 public string Name { get; set; }
88
99 public void MoveTo(string street, string city)
1010 {
11− _address.Street = street;
12− this._address.City = city;
11+ Street = street;
12+ this.City = city;
1313 }
1414
15− public string Label() => Name + "\n" + _address.Format();
15+ public string Label() => Name + "\n" + Format();
16+
17+ public string Street { get; set; }
18+
19+ public string City { get; set; }
20+
21+ // One line, for labels.
22+ public string Format() => Street + ", " + City + ", " + _country;
1623 }
1724 }

#holder-escapes-rejected

A use of the holding field other than to reach a member of the inlined class has nothing to become once the object is gone

refusal
Targetsymbol T:Shop.Address
Refusesholder-escapes; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6 }
7 }
Customer.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Customer
6 {
7 private readonly Address _address = new Address();
8
9 public void Print()
10 {
11 Console.WriteLine(_address);
12 }
13 }
14 }

#holder-not-created-rejected

The holding field must create the object in its initializer, so there is exactly one for each instance of the holding class from the start

refusal
Targetsymbol T:Shop.Address
Refusesholder-not-created; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6 }
7 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address;
6
7 public Customer()
8 {
9 _address = new Address();
10 }
11
12 public string Label() => _address.Street;
13 }
14 }

#holder-property-used-elsewhere

A get-only property can hold the inlined class; uses through it in other files go to the holding class directly, and the usings the moved members need are added

success
Targetsymbol T:Shop.Address
Address.csdeleted
1−using System.Collections.Generic;
2−
3−namespace Shop
4−{
5− public class Address
6− {
7− public List<string> Lines { get; } = new List<string>();
8−
9− public string Format() => string.Join(", ", Lines);
10− }
11−}
Customer.csmodified
1+using System.Collections.Generic;
2+
13 namespace Shop
24 {
35 public class Customer
46 {
5− public Address Address { get; } = new Address();
7+ public List<string> Lines { get; } = new List<string>();
8+
9+ public string Format() => string.Join(", ", Lines);
610 }
711 }
Labels.csmodified
11 namespace Shop
22 {
33 public class Labels
44 {
55 public string For(Customer customer)
66 {
7− customer.Address.Lines.Add("1 High Street");
8− return customer.Address.Format();
7+ customer.Lines.Add("1 High Street");
8+ return customer.Format();
99 }
1010 }
1111 }

#local-hides-member

A use through the holder where a parameter has the member's name becomes a use through this, so it still reaches the member

success
Targetsymbol T:Shop.Address
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private readonly Address _address = new Address();
5+ public string city;
66
77 public void Update(string city)
88 {
9− _address.city = city;
9+ this.city = city;
1010 }
1111 }
12−
13− public class Address
14− {
15− public string city;
16− }
1712 }

#member-exists-rejected

A member of the inlined class whose name the holding class already uses would clash

refusal
Targetsymbol T:Shop.Address
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 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public string Street { get; set; }
8
9 public string Label() => Street + " / " + _address.Street;
10 }
11 }

#no-holder-rejected

A class that no field or property holds has no class to be inlined into

refusal
Targetsymbol T:Shop.Address
Refusesno-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 }

#recipe

The recipe for a class without state: Move Instance Method into the class holding it, then Safe Delete Member on the holding field and Safe Delete Type on the class

successrecipe
Recipe
  1. move-instance-method on symbol M:Shop.PriceCalculator.Discounted(System.Decimal)
    into"Order"
  2. safe-delete-member on symbol F:Shop.Order._calculator
  3. safe-delete-type on symbol T:Shop.PriceCalculator
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
5− private readonly PriceCalculator _calculator = new PriceCalculator();
6−
75 public decimal Total(decimal subtotal)
86 {
9− return _calculator.Discounted(subtotal);
7+ return Discounted(subtotal);
108 }
9+
10+ public decimal Discounted(decimal amount) => amount * 0.9m;
1111 }
1212 }
PriceCalculator.csdeleted
1−namespace Shop
2−{
3− public class PriceCalculator
4− {
5− public decimal Discounted(decimal amount) => amount * 0.9m;
6− }
7−}

#recipe-with-state

The recipe for a class with state: each member moves into the holding class, a method before the members it uses; the private field it raised to internal is made private again; then the holding field and the class are deleted

successrecipe
Recipe
  1. move-instance-method on symbol M:Shop.Address.Format
    into"Customer"
  2. move-property on symbol P:Shop.Address.Street
    into"Customer"
  3. move-property on symbol P:Shop.Address.City
    into"Customer"
  4. move-field on symbol F:Shop.Address._country
    into"Customer"
  5. change-accessibility on symbol F:Shop.Customer._country
    accessibility"private"
  6. safe-delete-member on symbol F:Shop.Customer._address
  7. safe-delete-type on symbol T:Shop.Address
Address.csdeleted
1−namespace Shop
2−{
3− public class Address
4− {
5− private readonly string _country = "UK";
6−
7− // One line, for labels.
8− public string Format() => Street + ", " + City + ", " + _country;
9−
10− public string Street { get; set; }
11−
12− public string City { get; set; }
13− }
14−}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private readonly Address _address = new Address();
5+ private readonly string _country = "UK";
66
77 public string Name { get; set; }
88
99 public void MoveTo(string street, string city)
1010 {
11− _address.Street = street;
12− this._address.City = city;
11+ Street = street;
12+ this.City = city;
1313 }
1414
15− public string Label() => Name + "\n" + _address.Format();
15+ public string Label() => Name + "\n" + Format();
16+
17+ // One line, for labels.
18+ public string Format() => Street + ", " + City + ", " + _country;
19+
20+ public string Street { get; set; }
21+
22+ public string City { get; set; }
1623 }
1724 }

#same-file-nullable

A class declared in the same file as its holder is removed from it, and nullable annotations move with its members

success
Targetsymbol T:Shop.Address
Projectnullable enable
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private readonly Address _address = new();
5+ public string Label() => Street ?? "no address";
66
7− public string Label() => _address.Street ?? "no address";
8− }
9−
10− public class Address
11− {
127 public string? Street { get; set; }
138 }
149
1510 public class Order
1611 {
1712 public decimal Total { get; set; }
1813 }
1914 }

#several-holders-rejected

A class held by several fields would need a copy of its members for each

refusal
Targetsymbol T:Shop.Address
Refusesseveral-holders; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6 }
7 }
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 Label() => _home.Street + " / " + _work.Street;
9 }
10 }

#stateless-class

A class without state moves into the class holding it; its methods stay instance methods and are called directly

success
Targetsymbol T:Shop.PriceCalculator
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
5− private readonly PriceCalculator _calculator = new PriceCalculator();
6−
75 public decimal Total(decimal subtotal)
86 {
9− return _calculator.Discounted(subtotal);
7+ return Discounted(subtotal);
108 }
9+
10+ public decimal Discounted(decimal amount) => amount * 0.9m;
1111 }
1212 }
PriceCalculator.csdeleted
1−namespace Shop
2−{
3− public class PriceCalculator
4− {
5− public decimal Discounted(decimal amount) => amount * 0.9m;
6− }
7−}

#type-referenced-rejected

A class that other code names, here as a parameter type, cannot be deleted

refusal
Targetsymbol T:Shop.Address
Refusestype-referenced; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Street { get; set; }
6 }
7 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public string Label() => Describe(_address);
8
9 private static string Describe(Address address) => address.Street;
10 }
11 }

#unsupported-class-rejected

A class with a constructor, a base class or interfaces carries behaviour that plain members of the holding class would not

refusal
Targetsymbol T:Shop.Address
Refusesunsupported-class; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public Address()
6 {
7 Street = "unknown";
8 }
9
10 public string Street { get; set; }
11 }
12 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 private readonly Address _address = new Address();
6
7 public string Label() => _address.Street;
8 }
9 }

#with-state

A class with state and a method using it moves into the class holding it: a private field stays private, and the members keep their order

success
Targetsymbol T:Shop.Address
Address.csdeleted
1−namespace Shop
2−{
3− public class Address
4− {
5− private readonly string _country = "UK";
6−
7− // One line, for labels.
8− public string Format() => Street + ", " + City + ", " + _country;
9−
10− public string Street { get; set; }
11−
12− public string City { get; set; }
13− }
14−}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private readonly Address _address = new Address();
5+ private readonly string _country = "UK";
66
77 public string Name { get; set; }
88
99 public void MoveTo(string street, string city)
1010 {
11− _address.Street = street;
12− this._address.City = city;
11+ Street = street;
12+ this.City = city;
1313 }
1414
15− public string Label() => Name + "\n" + _address.Format();
15+ public string Label() => Name + "\n" + Format();
16+
17+ // One line, for labels.
18+ public string Format() => Street + ", " + City + ", " + _country;
19+
20+ public string Street { get; set; }
21+
22+ public string City { get; set; }
1623 }
1724 }