Auto-Implemented Properties · How it works

5 min read
Mid-level8 min read
Rapid overview

How it works

Basics

Q: What is an auto-implemented property?

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 (ldfld vs call 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.
Q: Why use an auto-property instead of a public field?

A: A property is a pair of methods, a field is raw storage, and they are not interchangeable once published:

Q: What does an auto-property look like in memory?

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

DeclarationWho can set itWhen
{ get; }only the declaring typein a constructor or the property initializer; the backing field is readonly
{ get; private set; }only the declaring typeat any time, from any member
{ get; init; } (C# 9)anyone who can see itduring construction: constructors, object initializers new C { X = 1 }, and with expressions
{ get; set; }anyone who can see itat any time
Q: What is the difference between { 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 error
Q: What does required 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.

Q: What is a property initializer and when does it run?

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)

Q: What is the 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

Q: What is the difference between { 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); // 0
Q: Why does order.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.

Q: Does a get-only collection property make the collection immutable?

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.

Q: Can you put an attribute on the generated backing field?

A: Yes, with the field: target: [field: NonSerialized] public int Cache { get; set; }. It is useful for serializers and interop that look at fields.

Q: Can interfaces and abstract classes declare auto-properties?

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

Q: When should a property be an auto-property, and when a full property?

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);).