RefactorMCP catalog

Convert Anonymous Type to Class

Replaces an anonymous type with a named class that behaves the same, and constructs that class wherever the containing member creates the anonymous type.

Arguments

ArgumentMeaning
namethe name of the new class

The target is a caret on the new of an anonymous object creation.

Precondition

  • The caret is on an anonymous object creation.
  • Every property type can be named: none is itself an anonymous type or uses a type parameter.
  • No type named name is already visible where the class would be declared.

Transformation

  • A sealed class named name is declared right after the type containing the creation, internal at namespace level or private when nested, with:
  • a constructor taking each property, camel case, in order;
  • a get-only property for each anonymous property, with the same type and nullable annotation;
  • Equals(object), GetHashCode and ToString matching the anonymous type's: equal when every property is equal by its default comparer, and printed as { Name = value, ... }.
  • Every creation of the same anonymous type in the containing member becomes new Name(...) with the property values in order, explicit or projected.
  • using System; and using System.Collections.Generic; are added when the file does not already import them.

Preserved

  • Property access, equality (so Distinct, GroupBy and dictionary lookups behave the same), hashing into the same buckets, ToString output, and ==, which compares references for both.

Limitations

  • Creations of the same anonymous type in other members are left anonymous, so where they meet the converted one the code no longer compiles and the conversion is refused.
  • Comments inside the anonymous initializer are not kept; the arguments are written on one line.
  • Hash codes are equal for equal instances but not the same numbers.

Error codes

CodeMeaning
no-anonymous-typethe caret is not on an anonymous object creation
unnamable-property-typea property's type is anonymous or uses a type parameter
name-conflicta type named name is already visible there

Cases

·

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

#comments-around-creation

Keeps the comments before and after the creation, and puts the class before the next type in the file

success
TargetHeader.cs, the /*^*/ caret
Arguments
name"Entry"
Header.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Header
77 {
88 public string Render(string title, int count)
99 {
1010 // One entry per page.
11− var entry = new/*^*/ { Title = title, Count = count }; // shown at the top
11+ var entry = new Entry(title, count); // shown at the top
1212 return entry.ToString();
1313 }
14+ }
15+
16+ internal sealed class Entry
17+ {
18+ public Entry(string title, int count)
19+ {
20+ Title = title;
21+ Count = count;
22+ }
23+
24+ public string Title { get; }
25+
26+ public int Count { get; }
27+
28+ public override bool Equals(object obj)
29+ {
30+ return obj is Entry other
31+ && EqualityComparer<string>.Default.Equals(Title, other.Title)
32+ && EqualityComparer<int>.Default.Equals(Count, other.Count);
33+ }
34+
35+ public override int GetHashCode() => HashCode.Combine(Title, Count);
36+
37+ public override string ToString() => $"{{ Title = {Title}, Count = {Count} }}";
1438 }
1539
1640 public class Footer
1741 {
1842 }
1943 }

#local-in-method

Declares a sealed class with the anonymous type's properties, value equality and ToString after the containing class, and constructs it instead

success
TargetReport.cs, the /*^*/ caret
Arguments
name"Line"
Report.csmodified
1+using System;
2+using System.Collections.Generic;
3+
14 namespace Shop;
25
36 public class Report
47 {
58 public string Summary(string customer, int quantity)
69 {
7− var line = /*^*/new { Customer = customer, Quantity = quantity };
10+ var line = new Line(customer, quantity);
811 return line.Customer + ": " + line.Quantity;
912 }
1013 }
14+
15+internal sealed class Line
16+{
17+ public Line(string customer, int quantity)
18+ {
19+ Customer = customer;
20+ Quantity = quantity;
21+ }
22+
23+ public string Customer { get; }
24+
25+ public int Quantity { get; }
26+
27+ public override bool Equals(object obj)
28+ {
29+ return obj is Line other
30+ && EqualityComparer<string>.Default.Equals(Customer, other.Customer)
31+ && EqualityComparer<int>.Default.Equals(Quantity, other.Quantity);
32+ }
33+
34+ public override int GetHashCode() => HashCode.Combine(Customer, Quantity);
35+
36+ public override string ToString() => $"{{ Customer = {Customer}, Quantity = {Quantity} }}";
37+}

#name-conflict-rejected

Refuses a class name that is already taken where the class would be declared

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Line"
Refusesname-conflict; every file is left unchanged
Report.csinput
1 namespace Shop;
2
3 public class Line
4 {
5 }
6
7 public class Report
8 {
9 public string Summary(string customer, int quantity)
10 {
11 var line = /*^*/new { Customer = customer, Quantity = quantity };
12 return line.Customer + ": " + line.Quantity;
13 }
14 }

#nested-anonymous-type-rejected

Refuses an anonymous type with a property of another anonymous type, which the class could not name

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Line"
Refusesunnamable-property-type mentioning “Total”; every file is left unchanged
Report.csinput
1 namespace Shop;
2
3 public class Report
4 {
5 public object Summary(string customer, decimal net, decimal tax)
6 {
7 return /*^*/new { Customer = customer, Total = new { Net = net, Tax = tax } };
8 }
9 }

#not-anonymous-rejected

Refuses when the caret is not on an anonymous object creation

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Line"
Refusesno-anonymous-type; every file is left unchanged
Report.csinput
1 using System.Text;
2
3 namespace Shop;
4
5 public class Report
6 {
7 public string Summary(string customer) => /*^*/new StringBuilder(customer).ToString();
8 }

#nullable-enabled

Keeps nullable annotations on the properties and annotates Equals when nullable annotations are enabled

success
TargetContacts.cs, the /*^*/ caret
Arguments
name"Contact"
Projectnullable enable
Contacts.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop;
55
66 public class Contacts
77 {
88 public string Describe(string name, string? email)
99 {
10− var contact = /*^*/new { Name = name, Email = email };
10+ var contact = new Contact(name, email);
1111 return contact.Name + (contact.Email ?? "");
1212 }
1313 }
14+
15+internal sealed class Contact
16+{
17+ public Contact(string name, string? email)
18+ {
19+ Name = name;
20+ Email = email;
21+ }
22+
23+ public string Name { get; }
24+
25+ public string? Email { get; }
26+
27+ public override bool Equals(object? obj)
28+ {
29+ return obj is Contact other
30+ && EqualityComparer<string>.Default.Equals(Name, other.Name)
31+ && EqualityComparer<string?>.Default.Equals(Email, other.Email);
32+ }
33+
34+ public override int GetHashCode() => HashCode.Combine(Name, Email);
35+
36+ public override string ToString() => $"{{ Name = {Name}, Email = {Email} }}";
37+}

#query-projection-keeps-equality

Replaces every creation of the same anonymous type in the method, including projection initializers, so Distinct and Equals still compare values

success
TargetReports.cs, the /*^*/ caret
Arguments
name"Row"
Reports.csmodified
1+using System;
12 using System.Collections.Generic;
23 using System.Linq;
34
45 namespace Shop;
56
67 public class Order
78 {
89 public string Customer { get; set; } = "";
910
1011 public decimal Total { get; set; }
1112 }
1213
1314 public class Reports
1415 {
1516 public int DistinctTotals(List<Order> orders)
1617 {
17− var rows = orders.Select(o => /*^*/new { o.Customer, o.Total }).Distinct();
18− var empty = new { Customer = "", Total = 0m };
18+ var rows = orders.Select(o => new Row(o.Customer, o.Total)).Distinct();
19+ var empty = new Row("", 0m);
1920 return rows.Count(r => !r.Equals(empty));
2021 }
2122
2223 public object Other() => new { Customer = "x", Total = 1m };
2324 }
25+
26+internal sealed class Row
27+{
28+ public Row(string customer, decimal total)
29+ {
30+ Customer = customer;
31+ Total = total;
32+ }
33+
34+ public string Customer { get; }
35+
36+ public decimal Total { get; }
37+
38+ public override bool Equals(object obj)
39+ {
40+ return obj is Row other
41+ && EqualityComparer<string>.Default.Equals(Customer, other.Customer)
42+ && EqualityComparer<decimal>.Default.Equals(Total, other.Total);
43+ }
44+
45+ public override int GetHashCode() => HashCode.Combine(Customer, Total);
46+
47+ public override string ToString() => $"{{ Customer = {Customer}, Total = {Total} }}";
48+}

#type-parameter-rejected

Refuses an anonymous type whose property type is a method's type parameter

refusal
TargetPairs.cs, the /*^*/ caret
Arguments
name"Pair"
Refusesunnamable-property-type mentioning “First”; every file is left unchanged
Pairs.csinput
1 namespace Shop;
2
3 public static class Pairs
4 {
5 public static object Make<T>(T first, T second) => /*^*/new { First = first, Second = second };
6 }