RefactorMCP catalog

Convert Setter to Init-Only

Replaces a property's set accessor with an init accessor, so the property can only be set while the object is being created.

Precondition

  • The property has a set accessor.
  • It is not virtual, abstract or an override, since every property in the hierarchy would have to change together.
  • Every assignment of the property, in any file, is one an init accessor allows: in an object initialiser, in a with expression, or in a constructor or init accessor of the declaring type or a type derived from it, through this or base. Compound assignments and increments after construction are refused like any other write.

Transformation

  • The set keyword becomes init. The accessor keeps its modifiers, such as protected, its attributes and its body.
  • Everything else about the property, including its initialiser and documentation, is unchanged.

Preserved

  • Every assignment the code makes, since each is one an init accessor accepts.

Limitations

  • Requires C# 9 or later.
  • Code outside the solution that sets the property after construction stops compiling.

Error codes

CodeMeaning
no-setterthe property has no set accessor
in-hierarchythe property is virtual, abstract or an override
assigned-after-constructionsome code sets the property after construction

Cases

·

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

#assigned-after-construction-rejected

Refuses a property that code assigns after the object is constructed

refusal
Targetsymbol P:Shop.Customer.Name
Refusesassigned-after-construction; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name { get; set; } = "";
6 }
7
8 public class Admin
9 {
10 public void Rename(Customer customer, string name) => customer.Name = name;
11 }
12 }

#in-hierarchy-rejected

Refuses a virtual property, whose overrides would have to change too

refusal
Targetsymbol P:Shop.Product.Name
Refusesin-hierarchy; every file is left unchanged
Product.csinput
1 namespace Shop
2 {
3 public class Product
4 {
5 public virtual string Name { get; set; } = "";
6 }
7
8 public class Book : Product
9 {
10 public override string Name { get; set; } = "";
11 }
12 }

#incremented-after-construction-rejected

Refuses a property its own type increments after construction

refusal
Targetsymbol P:Shop.Counter.Count
Refusesassigned-after-construction; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 public int Count { get; set; }
6
7 public void Increment() => Count++;
8 }
9 }

#no-setter-rejected

Refuses a property with no setter

refusal
Targetsymbol P:Shop.Sample.Name
Refusesno-setter; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public string Name { get; } = "";
6 }
7 }

#nullable

A nullable property keeps its annotation

success
Targetsymbol P:Shop.Profile.Nickname
Projectnullable enable
Profile.csmodified
11 namespace Shop
22 {
33 public class Profile
44 {
5− public string? Nickname { get; set; }
5+ public string? Nickname { get; init; }
66
77 public static Profile Anonymous() => new Profile { Nickname = null };
88 }
99 }

#object-initializer-across-files

A setter only used by object initialisers, here in another file, becomes init

success
Targetsymbol P:Shop.Customer.Name
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− public string Name { get; set; } = "";
5+ public string Name { get; init; } = "";
66 }
77 }
Signup.csunchanged
11 namespace Shop
22 {
33 public class Signup
44 {
55 public Customer Register(string name) => new Customer { Name = name };
66 }
77 }

#protected-setter-in-constructors

A protected setter assigned by constructors, including a derived type's, becomes a protected init accessor, keeping its documentation

success
Targetsymbol P:Shop.Product.Code
Product.csmodified
11 namespace Shop
22 {
33 public class Product
44 {
55 public Product(string code)
66 {
77 Code = code;
88 }
99
1010 protected Product()
1111 {
1212 }
1313
1414 /// <summary>The stock code.</summary>
15− public string Code { get; protected set; } = "";
15+ public string Code { get; protected init; } = "";
1616 }
1717
1818 public class Book : Product
1919 {
2020 public Book(string isbn)
2121 {
2222 Code = "ISBN-" + isbn;
2323 }
2424 }
2525 }

#setter-body-and-with-expression

A setter with a body keeps it as an init accessor, and a with expression on a record may still set it

success
Targetsymbol P:Shop.Point.X
Point.csmodified
11 namespace Shop
22 {
33 public record Point
44 {
55 private int _x;
66
77 public int X
88 {
99 get => _x;
10− set => _x = value < 0 ? 0 : value;
10+ init => _x = value < 0 ? 0 : value;
1111 }
1212
1313 public Point Moved(int x) => this with { X = x };
1414 }
1515 }