RefactorMCP catalog

Convert Primary Constructor to Constructor

Replaces a class or struct's primary constructor with an explicit constructor, storing each captured parameter in a private field.

Arguments

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

Precondition

  • The type is a class or struct, not a record, with a primary constructor.
  • For every parameter the members capture, the field _name is free: no member of the type already has that name.

Transformation

  • The parameter list leaves the type's header, and arguments given to the base type become a base(...) call.
  • A parameter read or written by a member (captured) gets a private field named _name, readonly unless a member assigns the parameter. The fields are declared first, in parameter order, and every use of the parameter in a member reads the field.
  • The constructor is public, takes the same parameters with their defaults and attributes, and goes after the fields. Its body assigns each captured parameter to its field, then runs, in declaration order, each field or property initializer that read a parameter, as an assignment; those initializers are removed. Initializers that read no parameter stay.
  • The member after the fields is separated from them by a blank line.

Preserved

  • Construction, named arguments at call sites, and the value of every member.

Limitations

  • Initializers that read a parameter run in the constructor body, after the base constructor, rather than before it. This only shows when the base constructor calls a virtual member that reads them.
  • The field name is not configurable.

Error codes

CodeMeaning
unsupported-typethe type is a record or has several declarations
no-primary-constructorthe type has no primary constructor
name-conflicta member already has the name a captured parameter's field needs

Cases

·

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

#assigned-parameter-becomes-mutable-field

Keeps a field writable when the members assign the captured parameter

success
Targetsymbol T:Shop.Counter
Counter.csmodified
11 namespace Shop;
22
3−public class Counter(int start)
3+public class Counter
44 {
5− public int Next() => start++;
5+ private int _start;
6+
7+ public Counter(int start)
8+ {
9+ _start = start;
10+ }
11+
12+ public int Next() => _start++;
613 }

#base-constructor-arguments

Turns the arguments on the base type into a base(...) call

success
Targetsymbol T:Shop.Discount
Discount.csmodified
11 namespace Shop;
22
33 public abstract class Adjustment(decimal amount)
44 {
55 public decimal Amount { get; } = amount;
66 }
77
8−public sealed class Discount(decimal amount, string code) : Adjustment(-amount)
8+public sealed class Discount : Adjustment
99 {
10− public string Describe() => code + ": " + Amount;
10+ private readonly string _code;
11+
12+ public Discount(decimal amount, string code) : base(-amount)
13+ {
14+ _code = code;
15+ }
16+
17+ public string Describe() => _code + ": " + Amount;
1118 }

#captured-parameters-become-fields

Stores each captured parameter in a private readonly field assigned by an explicit constructor, and points the members at the fields

success
Targetsymbol T:Shop.OrderService
OrderService.csmodified
11 namespace Shop;
22
33 /// <summary>Places orders.</summary>
4−public class OrderService(IRepository repository, ILogger logger)
4+public class OrderService
55 {
6+ private readonly IRepository _repository;
7+ private readonly ILogger _logger;
8+
9+ public OrderService(IRepository repository, ILogger logger)
10+ {
11+ _repository = repository;
12+ _logger = logger;
13+ }
14+
615 public void Place(string order)
716 {
817 // Save first so a failed log still keeps the order.
9− repository.Save(order);
10− logger.Log("placed " + order);
18+ _repository.Save(order);
19+ _logger.Log("placed " + order);
1120 }
1221 }
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-struct

Converts a generic readonly struct with a constraint, in a block-scoped namespace

success
Targetsymbol T:Shop.Measure`1
Measure.csmodified
11 using System;
22
33 namespace Shop
44 {
5− public readonly struct Measure<T>(T value, string unit) where T : IFormattable
5+ public readonly struct Measure<T> where T : IFormattable
66 {
7− public string Format() => value.ToString("0.0", null) + " " + unit;
7+ private readonly T _value;
8+ private readonly string _unit;
9+
10+ public Measure(T value, string unit)
11+ {
12+ _value = value;
13+ _unit = unit;
14+ }
15+
16+ public string Format() => _value.ToString("0.0", null) + " " + _unit;
817 }
918 }

#initializers-move-into-constructor

Moves a property initializer that reads a parameter into the constructor, after the field assignments, keeping the other initializer

success
Targetsymbol T:Shop.Customer
Customer.csmodified
11 namespace Shop;
22
3−public class Customer(string name, decimal limit = 100m)
3+public class Customer
44 {
5+ private readonly decimal _limit;
56 private int _orders = 0;
67
7− public string Name { get; } = name.Trim();
8+ public Customer(string name, decimal limit = 100m)
9+ {
10+ _limit = limit;
11+ Name = name.Trim();
12+ }
813
9− public bool CanSpend(decimal amount) => _orders < 10 && amount <= limit;
14+ public string Name { get; }
15+
16+ public bool CanSpend(decimal amount) => _orders < 10 && amount <= _limit;
1017 }

#name-conflict-rejected

Refuses when the field a captured parameter needs is already a member name

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

#no-primary-constructor-rejected

Refuses a class with no primary constructor

refusal
Targetsymbol T:Shop.Greeter
Refusesno-primary-constructor; every file is left unchanged
Greeter.csinput
1 namespace Shop;
2
3 public class Greeter
4 {
5 public string Greet(string name) => "Hello, " + name;
6 }

#record-rejected

Refuses a positional record, whose parameters declare properties

refusal
Targetsymbol T:Shop.Point
Refusesunsupported-type; every file is left unchanged
Point.csinput
1 namespace Shop;
2
3 public record Point(int X, int Y);