RefactorMCP catalog

Convert Auto-Property to Property with Backing Field

Gives an auto-property an explicit private field and accessors that read and write it, so the accessors can later gain behaviour. The reverse of Convert to Auto-Property.

Precondition

  • The property is an auto-property: every accessor lacks a body, and the property is not abstract, extern or an interface member.
  • The field name, from the name argument or _camelCase of the property, is free in the type.

Transformation

  • A private field of the property's type is added after the type's existing fields, or first in the type. It is static when the property is, and readonly when the property has no set accessor, since constructors and init accessors may assign a readonly field. The property's initialiser moves to the field.
  • A get-only property becomes an expression-bodied property returning the field. Constructors that assigned the property, in any part of a partial type, assign the field instead.
  • Otherwise each accessor gets an expression body, one to a line: get => _name;, set => _name = value; or init => _name = value;, keeping its modifiers.
  • The property keeps its documentation, attributes and accessibility.

Preserved

  • Every read and write of the value, including object initialisers that use an init accessor.
  • Nullable annotations.

Limitations

  • Attributes on individual accessors are not carried over.
  • Attributes targeting the compiler-generated field, [field: ...], are not moved to the new field.

Error codes

CodeMeaning
not-auto-propertythe property has accessor bodies, or is abstract, extern or an interface member
name-conflictthe field name is taken

Cases

·

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

#abstract-property-rejected

Refuses an abstract property, which looks like an auto-property but has no storage

refusal
Targetsymbol P:Shop.Shape.Sides
Refusesnot-auto-property; every file is left unchanged
Shape.csinput
1 namespace Shop
2 {
3 public abstract class Shape
4 {
5 public abstract int Sides { get; }
6 }
7 }

#get-only-assigned-in-partial-constructor

A get-only auto-property gets a readonly field; the constructor, in another part of the partial type, assigns the field instead of the property

success
Targetsymbol P:Shop.Product.Code
Product.Create.csmodified
11 namespace Shop
22 {
33 public partial class Product
44 {
55 public Product(string code, decimal price)
66 {
7− Code = code;
7+ _code = code;
88 _price = price;
99 }
1010
1111 public string Describe() => Code + ": " + _price;
1212 }
1313 }
Product.csmodified
11 namespace Shop
22 {
33 public partial class Product
44 {
55 private decimal _price;
6+ private readonly string _code;
67
7− public string Code { get; }
8+ public string Code => _code;
89
910 public decimal Price() => _price;
1011 }
1112 }

#get-set-with-initializer

An auto-property gets a private backing field holding its initialiser, and accessors that read and write it

success
Targetsymbol P:Shop.Customer.Name
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− public string Name { get; set; } = "unknown";
5+ private string _name = "unknown";
6+
7+ public string Name
8+ {
9+ get => _name;
10+ set => _name = value;
11+ }
612
713 public string Greeting() => "Dear " + Name;
814 }
915 }

#init-accessor-nullable

An init accessor gets a readonly field, which init may assign, and the nullable annotation carries over

success
Targetsymbol P:Shop.Profile.Nickname
Projectnullable enable
Profile.csmodified
11 namespace Shop
22 {
33 public class Profile
44 {
5− public string? Nickname { get; init; }
5+ private readonly string? _nickname;
6+
7+ public string? Nickname
8+ {
9+ get => _nickname;
10+ init => _nickname = value;
11+ }
612 }
713
814 public class Signup
915 {
1016 public Profile Create(string name) => new Profile { Nickname = name };
1117 }
1218 }

#name-conflict-rejected

Refuses a field name another member already has

refusal
Targetsymbol P:Shop.Sample.Count
Refusesname-conflict mentioning “'_count'”; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 private int _count;
6
7 public int Count { get; set; }
8
9 public int Total() => Count + _count;
10 }
11 }

#not-auto-property-rejected

Refuses a property that already has accessor bodies

refusal
Targetsymbol P:Shop.Sample.Name
Refusesnot-auto-property; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public string Name => "fixed";
6 }
7 }

#private-setter-keeps-documentation

Accessor modifiers carry over to the new accessors, and the property keeps its documentation and attributes

success
Targetsymbol P:Shop.Counter.Value
Arguments
name"_current"
Counter.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Counter
66 {
7+ private int _current;
8+
79 /// <summary>The current count.</summary>
810 [Obsolete("Use Next")]
9− public int Value { get; private set; }
11+ public int Value
12+ {
13+ get => _current;
14+ private set => _current = value;
15+ }
1016
1117 public int Next() => ++Value;
1218 }
1319 }

#static-property

A static auto-property gets a static field

success
Targetsymbol P:Shop.Registry.Instances
Registry.csmodified
11 namespace Shop
22 {
33 public static class Registry
44 {
5− public static int Instances { get; set; }
5+ private static int _instances;
6+
7+ public static int Instances
8+ {
9+ get => _instances;
10+ set => _instances = value;
11+ }
612
713 public static void Register() => Instances++;
814 }
915 }