RefactorMCP catalog

Convert to Primary Constructor

Replaces a constructor that only hands its parameters to fields and properties with a C# 12 primary constructor. Private fields that merely hold a parameter are removed and their uses read the parameter directly; other members are initialized from it.

Arguments

None. The target is the class or struct, by symbol.

Precondition

  • The type is a class or struct, not a record, and the project's language version is C# 12 or later.
  • It has exactly one instance constructor, which is public, carries no attributes or documentation comment, and does not call this(...).
  • The constructor body only assigns parameters to fields or auto-properties of the type, one statement each, and every parameter is either assigned or passed to the base constructor.
  • After the change, every use of a removed field reads the parameter; none would bind to a local, a parameter or a member of the same name.

Transformation

  • The constructor's parameter list, with defaults and modifiers, follows the type's name and type parameters. A base(...) call becomes the argument list of the base type in the base list. The constructor is removed.
  • A field is removed and its uses, _field or this._field, become the parameter when it is private, has no attributes or comments, is assigned only in the constructor, is read only through this, and its parameter is assigned to nothing else and not passed to the base constructor.
  • Every other assigned member keeps its declaration and gains the parameter as its initializer.

Preserved

  • Construction and the value of every member. A captured parameter is mutable where a readonly field was not, but nothing assigned it before.

Limitations

  • A class with several constructors is refused rather than having the others chain to the primary one.
  • A captured parameter no longer has the field's name in debuggers and reflection.

Error codes

CodeMeaning
unsupported-typethe type is a record, an interface or already has a primary constructor
language-versionthe project's language version is older than C# 12
several-constructorsthe type has more than one instance constructor, or none
constructor-accessibilitythe constructor is not public
constructor-annotatedthe constructor has attributes or a documentation comment
constructor-has-logicthe constructor does more than assign its parameters to fields and auto-properties
parameter-shadoweda use of a removed field would bind to another symbol with the parameter's name

Cases

·

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

#base-constructor-arguments

Moves the base constructor call onto the base type in the base list

success
Targetsymbol T:Shop.Discount
Discount.csmodified
11 namespace Shop;
22
33 public abstract class Adjustment
44 {
55 protected Adjustment(decimal amount)
66 {
77 Amount = amount;
88 }
99
1010 public decimal Amount { get; }
1111 }
1212
13−public sealed class Discount : Adjustment
13+public sealed class Discount(decimal amount, string code) : Adjustment(-amount)
1414 {
15− private readonly string _code;
16−
17− public Discount(decimal amount, string code) : base(-amount)
18− {
19− _code = code;
20− }
21−
22− public string Describe() => _code + ": " + Amount;
15+ public string Describe() => code + ": " + Amount;
2316 }

#constructor-has-logic-rejected

Refuses a constructor that validates its argument as well as assigning it

refusal
Targetsymbol T:Shop.Money
Refusesconstructor-has-logic; every file is left unchanged
Money.csinput
1 using System;
2
3 namespace Shop;
4
5 public sealed class Money
6 {
7 private readonly decimal _amount;
8
9 public Money(decimal amount)
10 {
11 if (amount < 0)
12 throw new ArgumentOutOfRangeException(nameof(amount));
13
14 _amount = amount;
15 }
16
17 public decimal Amount => _amount;
18 }

#documented-constructor-rejected

Refuses a constructor with a documentation comment, which a primary constructor has nowhere to keep

refusal
Targetsymbol T:Shop.Greeter
Refusesconstructor-annotated; every file is left unchanged
Greeter.csinput
1 namespace Shop;
2
3 public class Greeter
4 {
5 private readonly string _greeting;
6
7 /// <summary>Creates a greeter.</summary>
8 /// <param name="greeting">The word to greet with.</param>
9 public Greeter(string greeting)
10 {
11 _greeting = greeting;
12 }
13
14 public string Greet(string name) => _greeting + ", " + name;
15 }

#field-read-from-another-instance-is-kept

Keeps a field that another instance's member reads, initializing it from the parameter, and keeps a commented field too

success
Targetsymbol T:Shop.Money
Money.csmodified
11 namespace Shop;
22
3−public sealed class Money
3+public sealed class Money(decimal amount, string currency)
44 {
5− private readonly decimal _amount;
5+ private readonly decimal _amount = amount;
66
77 // ISO 4217 code.
8− private readonly string _currency;
9−
10− public Money(decimal amount, string currency)
11− {
12− _amount = amount;
13− _currency = currency;
14− }
8+ private readonly string _currency = currency;
159
1610 public bool IsMore(Money other) => _amount > other._amount;
1711
1812 public string Currency() => _currency;
1913 }

#fields-become-captured-parameters

Replaces a constructor that only assigns private readonly fields with a primary constructor whose parameters the members use directly

success
Targetsymbol T:Shop.OrderService
OrderService.csmodified
11 namespace Shop;
22
33 /// <summary>Places orders.</summary>
4−public class OrderService
4+public class OrderService(IRepository repository, ILogger logger)
55 {
6− private readonly IRepository _repository;
7− private readonly ILogger _logger;
8−
9− public OrderService(IRepository repository, ILogger logger)
10− {
11− _repository = repository;
12− this._logger = logger;
13− }
14−
156 public void Place(string order)
167 {
178 // Save first so a failed log still keeps the order.
18− _repository.Save(order);
19− this._logger.Log("placed " + order);
9+ repository.Save(order);
10+ logger.Log("placed " + order);
2011 }
2112 }
Services.csunchanged
11 namespace Shop;
22
33 public interface IRepository
44 {
55 void Save(string order);
66 }
77
88 public interface ILogger
99 {
1010 void Log(string message);
1111 }
1212
1313 public static class Composition
1414 {
1515 public static OrderService Create(IRepository repository, ILogger logger) => new OrderService(repository, logger: logger);
1616 }

#generic-class

Puts the parameters of a generic class after its type parameters and before its constraint

success
Targetsymbol T:Shop.Shelf`1
Shelf.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop;
44
5−public class Shelf<T> where T : class
5+public class Shelf<T>(List<T> items) where T : class
66 {
7− private readonly List<T> _items;
8−
9− public Shelf(List<T> items)
10− {
11− _items = items;
12− }
13−
14− public T First() => _items.Count > 0 ? _items[0] : null;
7+ public T First() => items.Count > 0 ? items[0] : null;
158 }

#language-version-rejected

Refuses when the project's language version predates primary constructors on classes

refusal
Targetsymbol T:Shop.Greeter
ProjectlangVersion 11
Refuseslanguage-version; every file is left unchanged
Greeter.csinput
1 namespace Shop;
2
3 public class Greeter
4 {
5 private readonly string _greeting;
6
7 public Greeter(string greeting)
8 {
9 _greeting = greeting;
10 }
11
12 public string Greet(string name) => _greeting + ", " + name;
13 }

#non-public-constructor-rejected

Refuses a private constructor, since a primary constructor is public

refusal
Targetsymbol T:Shop.Token
Refusesconstructor-accessibility; every file is left unchanged
Token.csinput
1 namespace Shop;
2
3 public class Token
4 {
5 private readonly string _value;
6
7 private Token(string value)
8 {
9 _value = value;
10 }
11
12 public static Token Create() => new Token("t");
13
14 public string Value => _value;
15 }

#parameter-shadowed-rejected

Refuses when a method parameter has the name the captured parameter would take, so the field's uses would bind to it

refusal
Targetsymbol T:Shop.Greeter
Refusesparameter-shadowed mentioning “greeting”; every file is left unchanged
Greeter.csinput
1 namespace Shop;
2
3 public class Greeter
4 {
5 private readonly string _greeting;
6
7 public Greeter(string greeting)
8 {
9 _greeting = greeting;
10 }
11
12 public string Greet(string greeting) => _greeting + ", " + greeting;
13 }

#property-gets-initializer

Initializes a get-only property from its parameter and captures the parameter behind a private field, on C# 12

success
Targetsymbol T:Shop.Customer
ProjectlangVersion 12
Customer.csmodified
11 namespace Shop;
22
3−public class Customer
3+public class Customer(string name, decimal limit)
44 {
5− private readonly decimal _limit;
5+ public string Name { get; } = name;
66
7− public Customer(string name, decimal limit)
8− {
9− Name = name;
10− _limit = limit;
11− }
12−
13− public string Name { get; }
14−
15− public bool CanSpend(decimal amount) => amount <= _limit;
7+ public bool CanSpend(decimal amount) => amount <= limit;
168 }

#record-rejected

Refuses a record, whose parameter list would declare properties rather than plain parameters

refusal
Targetsymbol T:Shop.Point
Refusesunsupported-type; every file is left unchanged
Point.csinput
1 namespace Shop;
2
3 public record Point
4 {
5 public Point(int x)
6 {
7 X = x;
8 }
9
10 public int X { get; }
11 }

#several-constructors-rejected

Refuses a class with more than one constructor

refusal
Targetsymbol T:Shop.Range
Refusesseveral-constructors; every file is left unchanged
Range.csinput
1 namespace Shop;
2
3 public class Range
4 {
5 private readonly int _low;
6 private readonly int _high;
7
8 public Range(int low, int high)
9 {
10 _low = low;
11 _high = high;
12 }
13
14 public Range(int high) : this(0, high)
15 {
16 }
17
18 public int Length => _high - _low;
19 }

#struct

Gives a readonly struct a primary constructor that initializes its property

success
Targetsymbol T:Shop.Temperature
Temperature.csmodified
11 namespace Shop;
22
3−public readonly struct Temperature
3+public readonly struct Temperature(double celsius)
44 {
5− public Temperature(double celsius)
6− {
7− Celsius = celsius;
8− }
9−
10− public double Celsius { get; }
5+ public double Celsius { get; } = celsius;
116
127 public double Fahrenheit => Celsius * 9 / 5 + 32;
138 }