Auto-Implemented Properties · How it works
5 min readHow it works
Basics
A: A property declared with accessors but no bodies. The compiler generates a private backing field and the accessor methods that read and write it.
public class Customer
{
public string Name { get; set; }
}
// Roughly what the compiler emits
public class Customer
{
[CompilerGenerated]
private string <Name>k__BackingField; // name is not legal C#, so you cannot collide with it
public string Name
{
get => <Name>k__BackingField;
set => <Name>k__BackingField = value;
}
}- Binary compatibility: changing a public field to a property later changes the compiled contract (
ldfldvscall get_Name), so every assembly compiled against it must be recompiled. Starting with an auto-property lets you add validation or logic later without breaking callers. - Interfaces can declare properties, not fields.
- Serialization and binding:
System.Text.Json, model binding, EF Core, WPF/MAUI binding and most mappers work on properties by default. - Different access levels per accessor (
{ get; private set; }),init,required,virtual/override. - Debugging: you can set a breakpoint on an accessor.
A: A property is a pair of methods, a field is raw storage, and they are not interchangeable once published:
A: Exactly like a field. The object's layout contains the backing field; there is no extra storage for the property itself. The accessors are ordinary methods, but they are so small that the JIT inlines them, so customer.Name compiles down to the same field load a public field would. For a struct, the backing fields are part of the struct's inline layout.
Accessor variations
| Declaration | Who can set it | When |
|---|---|---|
{ get; } | only the declaring type | in a constructor or the property initializer; the backing field is readonly |
{ get; private set; } | only the declaring type | at any time, from any member |
{ get; init; } (C# 9) | anyone who can see it | during construction: constructors, object initializers new C { X = 1 }, and with expressions |
{ get; set; } | anyone who can see it | at any time |
{ get; }, { get; private set; } and { get; init; }?A:
init gives immutable objects that are still easy to create with object-initializer syntax, and it is what positional records generate.
public class Order
{
public Guid Id { get; } = Guid.NewGuid(); // get-only, set once
public string Status { get; private set; } = "New";
public required string CustomerId { get; init; } // C# 11: caller must set it
public void Ship() => Status = "Shipped"; // only Order changes Status
}
var o = new Order { CustomerId = "C-1" }; // omitting CustomerId is a compile errorrequired do?A: (C# 11) The compiler forces every object creation to set the member in an object initializer, or it reports an error. It fixes the problem that init alone does not make a property mandatory. A constructor marked [SetsRequiredMembers] tells the compiler that it sets them itself. required is a compile-time check; deserializers such as System.Text.Json (.NET 7+) honour it at runtime too.
A: public List<string> Tags { get; } = new(); (C# 6) assigns the backing field once per instance, when the object is constructed, before the constructor body runs (in declaration order, like field initializers). The initializer cannot reference this or other instance members.
The field keyword (C# 14)
field keyword?A: Inside a property accessor, field refers to the compiler-generated backing field. It lets you add logic to one accessor while keeping the auto-property's generated storage, instead of declaring a private field yourself. It was a preview in C# 13 and shipped in C# 14 (.NET 10).
// Before: full property with a hand-written field
private string _name = "";
public string Name
{
get => _name;
set => _name = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
// C# 14: semi-auto property
public string Name
{
get;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
} = "";
If a type already has a member called field, use @field or this.field to refer to it inside accessors.
Traps
{ get; } = new List<T>() and => new List<T>()?A: The first is an auto-property with an initializer: one list is created when the object is built and the same list is returned every time. The second is an expression-bodied property with no storage: every read creates a new, empty list, so adding to it does nothing lasting.
public List<string> A { get; } = new(); // one list
public List<string> B => new(); // new list on every access
obj.A.Add("x"); Console.WriteLine(obj.A.Count); // 1
obj.B.Add("x"); Console.WriteLine(obj.B.Count); // 0order.Position.X = 5; fail when Position is a struct property?A: The getter returns a copy of the struct, and modifying a temporary copy is pointless, so the compiler rejects it (CS1612). Assign a whole new value instead: order.Position = order.Position with { X = 5 };. With a public field of struct type the same line compiles and mutates in place, one of the few observable differences between fields and properties.
A: No. public List<string> Tags { get; } = new(); stops callers from replacing the list, but they can still Add, Remove and Clear. To protect it, keep a private List<T> and expose IReadOnlyList<T> (or IReadOnlyCollection<T>), with methods on the type that change it. This is the usual DDD aggregate pattern.
A: Yes, with the field: target: [field: NonSerialized] public int Cache { get; set; }. It is useful for serializers and interop that look at fields.
A: An interface declares string Name { get; } as a contract with no storage (interfaces cannot have instance fields). An abstract or virtual auto-property in a class can be overridden; an override may be a full property with its own logic.
When to use which
A: Auto-property when the property simply stores a value (DTOs, options classes, most entity properties). Use init or get-only for data that should not change after construction, private set when only the type's own methods may change it, and required when the caller must supply it. Switch to a full property, or field in C# 14, when you need validation, normalization, change notification (INotifyPropertyChanged), lazy loading, or a value computed from other state. A computed value with no storage of its own belongs in an expression-bodied property (public decimal Total => Lines.Sum(l => l.Amount);).