RefactorMCP catalog

Encapsulate Collection

Stops callers changing a type's list behind its back: the list is exposed as a read-only view, and the type gains Add and Remove methods for the changes it allows.

Precondition

  • The target is a private field of type List<T>.
  • A property that exposes it, if there is one, only has a getter; a setter could replace the list.
  • The element name, from the name argument or the property name made singular, gives Add<Element> and Remove<Element> names that are free, as is the property name when a property has to be created.
  • After the change, every use of the property still compiles. Callers that read, count, index or enumerate the list are unaffected, and calls to Add or Remove through the property are redirected. Any other change, such as Clear() or passing the list where a List<T> is expected, is refused and nothing is changed.

Transformation

  • The property whose getter returns the field becomes an IReadOnlyList<T> returning _field.AsReadOnly(), keeping its form, documentation and position. When no property exposes the field, one named after it (_tags gives Tags) is added after the type's fields.
  • public void Add<Element>(T element) and public bool Remove<Element>(T element) are added after the property, delegating to the list. Remove returns whether an element was removed, as List<T>.Remove does.
  • The element name drops a plural ending: Tags gives Tag and Entries gives Entry. Irregular plurals need the name argument.
  • order.Tags.Add(x) becomes order.AddTag(x) and order.Tags.Remove(x) becomes order.RemoveTag(x), in any file and inside the type.
  • Code inside the type that uses the field directly is unchanged.

Preserved

  • The contents of the list after every call, and every value callers read.
  • Nullable annotations on the element type.

Limitations

  • Only List<T> is supported; sets, dictionaries and arrays are refused.
  • The view is a live read-only wrapper, not a copy, so callers see later changes.
  • Code outside the solution that changed the list through the property stops compiling.

Error codes

CodeMeaning
field-not-privatethe field is visible outside the type
unsupported-collection-typethe field is not a List<T>
property-has-setterthe exposing property can replace the list
name-conflicta member name the refactoring needs is taken
unsupported-usecode changes the list through the property in a way other than Add or Remove

Cases

·

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

#block-getter-keeps-documentation

A block-bodied getter is rewritten in place and keeps the property's documentation; the element name comes from the name argument

success
Targetsymbol F:Shop.Team._people
Arguments
name"Member"
Team.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Team
66 {
77 private readonly List<string> _people = new List<string>();
88
99 /// <summary>Everyone on the team.</summary>
10− public List<string> People
10+ public IReadOnlyList<string> People
1111 {
12− get { return _people; }
12+ get { return _people.AsReadOnly(); }
1313 }
14+
15+ public void AddMember(string member) => _people.Add(member);
16+
17+ public bool RemoveMember(string member) => _people.Remove(member);
1418 }
1519
1620 public class Rota
1721 {
18− public void Join(Team team, string name) => team.People.Add(name);
22+ public void Join(Team team, string name) => team.AddMember(name);
1923 }
2024 }

#exposed-list-across-files

The property exposing a list returns a read-only view, Add and Remove methods are added, and callers in other files that added and removed through the property call them

success
Targetsymbol F:Shop.Order._tags
Order.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 private readonly List<string> _tags = new List<string>();
88
9− public List<string> Tags => _tags;
9+ public IReadOnlyList<string> Tags => _tags.AsReadOnly();
10+
11+ public void AddTag(string tag) => _tags.Add(tag);
12+
13+ public bool RemoveTag(string tag) => _tags.Remove(tag);
1014
1115 public bool IsGift() => _tags.Contains("gift");
1216 }
1317 }
Tagging.csmodified
11 namespace Shop
22 {
33 public class Tagging
44 {
55 public int Retag(Order order)
66 {
7− order.Tags.Add("gift");
8− order.Tags.Remove("sale");
7+ order.AddTag("gift");
8+ order.RemoveTag("sale");
99 var count = 0;
1010 foreach (var tag in order.Tags)
1111 count += tag.Length;
1212 return count + order.Tags.Count;
1313 }
1414 }
1515 }

#field-not-private-rejected

Refuses a list field that is visible outside the type; encapsulate the field first

refusal
Targetsymbol F:Shop.Order.Tags
Refusesfield-not-private; every file is left unchanged
Order.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Order
6 {
7 public List<string> Tags = new List<string>();
8 }
9 }

#generic-element-type

A list of a type parameter gives methods taking that type parameter

success
Targetsymbol F:Shop.Basket`1._items
Basket.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Basket<T>
66 {
77 private readonly List<T> _items = new List<T>();
88
9− public List<T> Items => _items;
9+ public IReadOnlyList<T> Items => _items.AsReadOnly();
10+
11+ public void AddItem(T item) => _items.Add(item);
12+
13+ public bool RemoveItem(T item) => _items.Remove(item);
1014 }
1115
1216 public class Till
1317 {
1418 public int Scan(Basket<int> basket)
1519 {
16− basket.Items.Add(42);
20+ basket.AddItem(42);
1721 return basket.Items[0];
1822 }
1923 }
2024 }

#no-exposing-property

A list no property exposes gets a read-only property after the fields, then Add and Remove methods; singular names drop -ies to -y

success
Targetsymbol F:Shop.Journal._entries
Journal.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Journal
66 {
77 private readonly List<decimal> _entries = new List<decimal>();
8+
9+ public IReadOnlyList<decimal> Entries => _entries.AsReadOnly();
10+
11+ public void AddEntry(decimal entry) => _entries.Add(entry);
12+
13+ public bool RemoveEntry(decimal entry) => _entries.Remove(entry);
814
915 public decimal Balance()
1016 {
1117 var total = 0m;
1218 foreach (var entry in _entries)
1319 total += entry;
1420 return total;
1521 }
1622 }
1723 }

#not-a-list-rejected

Refuses a collection that is not a List<T>

refusal
Targetsymbol F:Shop.Order._codes
Refusesunsupported-collection-type; every file is left unchanged
Order.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Order
6 {
7 private readonly HashSet<string> _codes = new HashSet<string>();
8
9 public HashSet<string> Codes => _codes;
10 }
11 }

#nullable-elements

Nullable element types carry over to the view and the methods

success
Targetsymbol F:Shop.Survey._answers
Projectnullable enable
Survey.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Survey
66 {
77 private readonly List<string?> _answers = new List<string?>();
88
9− public List<string?> Answers => _answers;
9+ public IReadOnlyList<string?> Answers => _answers.AsReadOnly();
1010
11− public void Skip() => Answers.Add(null);
11+ public void AddAnswer(string? answer) => _answers.Add(answer);
12+
13+ public bool RemoveAnswer(string? answer) => _answers.Remove(answer);
14+
15+ public void Skip() => AddAnswer(null);
1216 }
1317 }

#property-has-setter-rejected

Refuses when the exposing property can also replace the list

refusal
Targetsymbol F:Shop.Order._tags
Refusesproperty-has-setter; every file is left unchanged
Order.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Order
6 {
7 private List<string> _tags = new List<string>();
8
9 public List<string> Tags
10 {
11 get => _tags;
12 set => _tags = value;
13 }
14 }
15 }

#unsupported-use-rejected

Refuses when a caller uses the list in a way a read-only view does not allow, and changes nothing

refusal
Targetsymbol F:Shop.Order._tags
Refusesunsupported-use mentioning “Clear”; every file is left unchanged
Order.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Order
6 {
7 private readonly List<string> _tags = new List<string>();
8
9 public List<string> Tags => _tags;
10 }
11
12 public class Tagging
13 {
14 public void Reset(Order order) => order.Tags.Clear();
15 }
16 }