RefactorMCP catalog

Introduce Parameter Object

Replaces a group of parameters that travel together with one parameter of a new positional record holding them. The body reads the record's properties, and every call builds the record from the arguments it passed.

Recipe

  1. change-signature on the method: the new list replaces the group with a tuple parameter whose element names are the grouped parameters' names, value builds the tuple at the call, and replacements turns each use of a grouped parameter into a read of its element.
  2. convert-tuple-to-named-type on the method with parameter naming the tuple parameter. The tuple becomes the record, the element reads become property reads, and the tuples calls pass become new expressions.
"steps": [
  {
    "refactoring": "change-signature",
    "target": { "symbol": "M:Bank.Account.TotalBetween(System.DateTime,System.DateTime)" },
    "arguments": {
      "parameters": [ { "name": "range", "type": "(DateTime start, DateTime end)", "value": "(from, to)" } ],
      "replacements": { "start": "range.start", "end": "range.end" }
    }
  },
  {
    "refactoring": "convert-tuple-to-named-type",
    "target": { "symbol": "M:Bank.Account.TotalBetween(System.ValueTuple{System.DateTime,System.DateTime})" },
    "arguments": { "name": "DateRange", "parameter": "range" }
  }
]

The plan's recipe starts with Create Type, but that primitive creates an empty type, and no primitive gives it the properties and constructor a parameter object needs. Convert Tuple to Named Type creates the record with them and rewrites the calls, so the recipe passes through a tuple instead. Change Signature passes one value to every call, so the recipe only describes a method with a single caller; the dedicated implementation builds each call's tuple from that call's own arguments.

Arguments

ArgumentMeaning
parametersthe parameters to group, in the order the record declares them
typeNamethe name of the new record
parameterNamethe name of the parameter that replaces the group
kindstruct for a readonly record struct (the default), class for a sealed record

The target is the method, by symbol.

Precondition

  • The target is an ordinary method, and at least two parameters are grouped.
  • Each grouped parameter exists, is passed by value, is required, is not a params array and is not the this of an extension method.
  • No parameter outside the group has the new parameter's name.
  • No type named typeName is visible where the record would be declared.
  • Change Signature's precondition holds: the method is only called, never converted to a delegate, and its family is declared in the solution.
  • The result compiles.

Transformation

  • The new parameter takes the place of the first grouped parameter; the others are removed.
  • The record is declared after the type containing the method, with that type's accessibility. Its properties are the grouped parameters' names in Pascal case, with their types, in the order given.
  • Each use of a grouped parameter in the body, and in the bodies of overrides and implementations, reads the matching property.
  • Each call passes new TypeName(...) built from the arguments it passed for the grouped parameters, keeping the names of other named arguments.

Preserved

  • The values the method sees: each property holds what the call passed for the parameter it replaces.
  • Arguments are evaluated in the order they were written.
  • Other parameters and their arguments, with their comments and layout.

Limitations

  • The record is declared in the method's file, not a file of its own.
  • The new argument is written positionally even when the call named the grouped arguments.
  • Constructors are not covered, since Convert Tuple to Named Type works on methods.

Error codes

CodeMeaning
too-few-parametersfewer than two parameters are grouped
unknown-parametera listed name is not a parameter of the method
unsupported-parametera grouped parameter is ref, out, params, optional or an extension's this
duplicate-parametera parameter outside the group has the new parameter's name
not-a-methodthe target is a constructor or other special method
type-name-conflicta type named typeName is already visible
method-group-referencethe method is used as a method group
external-memberthe method overrides or implements a member outside the solution

Cases

·

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

#across-files

Groups two parameters into a record, as the recipe does, updating a caller in another file

success
Targetsymbol M:Bank.Account.TotalBetween(System.DateTime,System.DateTime)
Arguments
parameters["start", "end"]
typeName"DateRange"
parameterName"range"
Account.csmodified
11 using System;
22 using System.Collections.Generic;
33 using System.Linq;
44
55 namespace Bank
66 {
77 public class Entry
88 {
99 public Entry(DateTime date, decimal amount)
1010 {
1111 Date = date;
1212 Amount = amount;
1313 }
1414
1515 public DateTime Date { get; }
1616
1717 public decimal Amount { get; }
1818 }
1919
2020 public class Account
2121 {
2222 private readonly List<Entry> _entries = new List<Entry>();
2323
24− public decimal TotalBetween(DateTime start, DateTime end)
24+ public decimal TotalBetween(DateRange range)
2525 {
26− return _entries.Where(e => e.Date >= start && e.Date <= end).Sum(e => e.Amount);
26+ return _entries.Where(e => e.Date >= range.Start && e.Date <= range.End).Sum(e => e.Amount);
2727 }
2828 }
29+
30+ public readonly record struct DateRange(DateTime Start, DateTime End);
2931 }
Statement.csmodified
11 using System;
22
33 namespace Bank
44 {
55 public class Statement
66 {
77 public decimal Monthly(Account account, DateTime from, DateTime to)
88 {
9− return account.TotalBetween(from, to);
9+ return account.TotalBetween(new DateRange(from, to));
1010 }
1111 }
1212 }

#optional-parameter-rejected

Refuses to group an optional parameter, whose calls may leave it out

refusal
Targetsymbol M:Shop.Report.Print(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
typeName"Band"
parameterName"band"
Refusesunsupported-parameter mentioning “'high'”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Print(int low, int high = 100)
6 {
7 return low + "-" + high;
8 }
9
10 public string Sample()
11 {
12 return Print(5);
13 }
14 }
15 }

#recipe

Runs the recipe: Change Signature to take the parameters as one tuple, reading its elements in the body, then Convert Tuple to Named Type

successrecipe
Recipe
  1. change-signature on symbol M:Bank.Account.TotalBetween(System.DateTime,System.DateTime)
    parameters[{"name": "range", "type": "(DateTime start, DateTime end)", "value": "(from, to)"}]
    replacements{"start": "range.start", "end": "range.end"}
  2. convert-tuple-to-named-type on symbol M:Bank.Account.TotalBetween(System.ValueTuple{System.DateTime,System.DateTime})
    name"DateRange"
    parameter"range"
Account.csmodified
11 using System;
22 using System.Collections.Generic;
33 using System.Linq;
44
55 namespace Bank
66 {
77 public class Entry
88 {
99 public Entry(DateTime date, decimal amount)
1010 {
1111 Date = date;
1212 Amount = amount;
1313 }
1414
1515 public DateTime Date { get; }
1616
1717 public decimal Amount { get; }
1818 }
1919
2020 public class Account
2121 {
2222 private readonly List<Entry> _entries = new List<Entry>();
2323
24− public decimal TotalBetween(DateTime start, DateTime end)
24+ public decimal TotalBetween(DateRange range)
2525 {
26− return _entries.Where(e => e.Date >= start && e.Date <= end).Sum(e => e.Amount);
26+ return _entries.Where(e => e.Date >= range.Start && e.Date <= range.End).Sum(e => e.Amount);
2727 }
2828 }
29+
30+ public readonly record struct DateRange(DateTime Start, DateTime End);
2931 }
Statement.csmodified
11 using System;
22
33 namespace Bank
44 {
55 public class Statement
66 {
77 public decimal Monthly(Account account, DateTime from, DateTime to)
88 {
9− return account.TotalBetween(from, to);
9+ return account.TotalBetween(new DateRange(from, to));
1010 }
1111 }
1212 }

#recipe-assigned-parameter-rejected

The recipe stops at its first step when Change Signature cannot replace a parameter the body assigns, and nothing changes

refusalrecipe
Recipe
  1. change-signature on symbol M:Shop.Report.Print(System.Int32,System.Int32)
    parameters[{"name": "band", "type": "(int low, int high)", "value": "(1, 5)"}]
    replacements{"low": "band.low", "high": "band.high"}
  2. convert-tuple-to-named-type on symbol M:Shop.Report.Print(System.ValueTuple{System.Int32,System.Int32})
    name"Band"
    parameter"band"
Refuseserror mentioning “Parameter 'low' is assigned in the body”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Print(int low, int high)
6 {
7 if (low < 0)
8 low = 0;
9 return low + "-" + high;
10 }
11
12 public string Sample()
13 {
14 return Print(1, 5);
15 }
16 }
17 }

#several-callers

Groups parameters in the middle of the list into a record class; each call builds it from its own arguments, named ones included

success
Targetsymbol M:Shop.Report.Print(System.String,System.Int32,System.Int32,System.Boolean)
Arguments
parameters["low", "high"]
typeName"Band"
parameterName"band"
kind"class"
Report.csmodified
11 namespace Shop
22 {
33 public class Report
44 {
5− public string Print(string title, int low, int high, bool detailed)
5+ public string Print(string title, Band band, bool detailed)
66 {
7− var range = low + "-" + high;
8− return detailed ? title + " " + range + " (" + (high - low) + ")" : title + " " + range;
7+ var range = band.Low + "-" + band.High;
8+ return detailed ? title + " " + range + " (" + (band.High - band.Low) + ")" : title + " " + range;
99 }
1010
1111 public string Summary()
1212 {
13− return Print("Small", 0, 10, false) + Print("Large", 10, 100, detailed: true);
13+ return Print("Small", new Band(0, 10), false) + Print("Large", new Band(10, 100), detailed: true);
1414 }
1515
1616 public string Custom(int from)
1717 {
18− return Print(title: "Custom", low: from, high: from * 2, detailed: false);
18+ return Print(title: "Custom", new Band(from, from * 2), detailed: false);
1919 }
2020 }
21+
22+ public sealed record Band(int Low, int High);
2123 }

#too-few-parameters-rejected

Refuses a group of one parameter, which a tuple cannot hold

refusal
Targetsymbol M:Shop.Report.Print(System.String,System.Int32)
Arguments
parameters["count"]
typeName"Count"
parameterName"value"
Refusestoo-few-parameters; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Print(string title, int count)
6 {
7 return title + count;
8 }
9 }
10 }

#type-name-conflict-rejected

Refuses a record name already in use, undoing the signature change made before the record would be declared

refusal
Targetsymbol M:Bank.Account.TotalBetween(System.DateTime,System.DateTime)
Arguments
parameters["start", "end"]
typeName"Entry"
parameterName"range"
Refusestype-name-conflict; every file is left unchanged
Account.csinput
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4
5 namespace Bank
6 {
7 public class Entry
8 {
9 public Entry(DateTime date, decimal amount)
10 {
11 Date = date;
12 Amount = amount;
13 }
14
15 public DateTime Date { get; }
16
17 public decimal Amount { get; }
18 }
19
20 public class Account
21 {
22 private readonly List<Entry> _entries = new List<Entry>();
23
24 public decimal TotalBetween(DateTime start, DateTime end)
25 {
26 return _entries.Where(e => e.Date >= start && e.Date <= end).Sum(e => e.Amount);
27 }
28 }
29 }
Statement.csinput
1 using System;
2
3 namespace Bank
4 {
5 public class Statement
6 {
7 public decimal Monthly(Account account, DateTime from, DateTime to)
8 {
9 return account.TotalBetween(from, to);
10 }
11 }
12 }