# Properties

Source: https://codewiki.com/csharp/properties/

> - **what**: A C# property exposes data with field-like syntax while `get`, `set`, or `init` accessors enforce its read and assignment contract.
> - **trap**: `init` doesn't provide deep immutability, and `required` isn't runtime validation; a collection getter can still leak mutable state.
> - **fix**: Use auto-properties for simple storage, implement accessors for validation, and make mutation, nullability, and ownership testable contracts.

## What it is and why it exists

A property is a member of a class, struct, or interface. Callers use field-like syntax such as `order.Total` or `order.Status = value`, while the declaring type controls reads and assignments through accessors. A property isn't necessarily a field: it can return stored data, compute a result, or reject an assignment.

Properties resolve the tension between exposing data and retaining control over it. Once a public field becomes part of an API, its declaring type can't add validation, computation, or stricter write access at the same access point. Paired `GetName()` and `SetName()` methods, meanwhile, make simple data access cumbersome. A property puts those control points behind stable member syntax.

You'll encounter properties in domain objects, configuration types, DTOs, serialization models, and UI binding objects. Similar call syntax doesn't imply a similar contract: an auto-property usually stores a value, a computed property evaluates on every read, and a custom accessor can run arbitrary code. Review the declaration, not the call syntax, to determine behavior.

A property should describe a characteristic an object has at a particular moment. A network request, database query, long computation, or visible state transition usually belongs in a method because a method call makes cost and action more apparent.

## How it works

### Accessors define read and write capabilities

A property accessor is the `get`, `set`, or `init` part of a property. Reading executes `get`, ordinary assignment executes `set`, and `init` permits assignment only at valid locations during object construction. The implicit `value` parameter in `set` and `init` is the new value supplied by the caller.

Accessor combinations form the public contract directly. A property with only `get` is read-only to callers; `get` plus `set` permits repeated assignment; `get` plus `init` permits construction-time assignment and rejects later ordinary assignment. C# also permits write-only properties, but APIs rarely need a shape whose state callers can't read back.

A property and its accessors can also have different accessibility. `public decimal Balance { get; private set; }` lets every caller read while only the declaring type can modify it. A stricter access modifier can appear on only one accessor, and the property must have both a read and a write accessor.

| Declaration | External read | External ordinary assignment | Object-initializer assignment |
| --- | --- | --- | --- |
| `{ get; set; }` | Yes | Yes | Yes |
| `{ get; private set; }` | Yes | No | No |
| `{ get; init; }` | Yes | No | Yes |
| `{ get; }` | Yes | No | No |
| `=> expression` | Yes | No | No |

### Stored and computed properties

An auto-property omits accessor bodies, as in `public string Name { get; set; }`. The compiler creates a hidden backing field and generates accessors that read and write that storage. A property initializer assigns the storage during construction.

When you need control logic, declare a backing field explicitly and reference it from the accessors. This works well for range checks, input normalization, and simple invariants. Validate first and then write the field; otherwise, an exception can leave the object partially updated.

A computed property doesn't need storage of its own. `public decimal Total => UnitPrice * Quantity;` evaluates from current state on every read, so no separate cache can become stale. If computation is expensive or requires I/O, use a clearly named method or design a cache with explicit invalidation rules.

C# 14's `field` contextual keyword provides a third shape. An accessor can use `field` to reach the compiler-synthesized backing field while another accessor remains auto-implemented. This removes the boilerplate of declaring a field solely for a small amount of validation logic.

### `init` and `required` answer different questions

An `init` accessor restricts where a property can be assigned. Callers can set it in an object initializer, and construction code can establish initial state; after the construction phase, ordinary code can't assign it again. This restricts property assignment but doesn't freeze the object referenced by the property.

A required member makes an object-creation expression initialize a field or property unless the chosen constructor declares that it satisfies all required members. `required` doesn't decide whether later mutation is allowed, so it can pair with either `set` or `init`. It also doesn't validate a string's nullness, a number's range, or relationships between fields.

Non-nullable reference types, `required`, and runtime validation each handle a different layer. A non-nullable annotation participates in compiler null-state analysis, `required` checks whether object-creation syntax omitted a member, and an accessor or constructor enforces actual runtime invariants. Robust boundary types often need all three rather than a choice among them.

### Field syntax hides method calls

After compilation, reading and writing a property correspond to specially named methods, commonly visible in metadata as `get_Name` and `set_Name`. Reflection exposes the property as property metadata and can also retrieve those accessors. Callers still write `customer.Name`; the compiler binds that syntax to accessor calls.

This is why properties can appear in interfaces and be declared `virtual` or `abstract`. An implementation or override supplies access behavior; it doesn't require every type to have a field with that name. It also explains why a debugger that evaluates a property can execute user code.

## Examples

### Start with auto-properties

The first example leaves simple state to auto-properties, expresses a derived value as a computed property, and puts inventory mutation in a method. `private set` prevents callers from bypassing `TrySell` to replace the stock directly.

<!-- quick -->

```csharp
// file: ProductInventory.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;

var item = new InventoryItem(openingStock: 3)
{
    Name = "Keyboard",
    UnitPrice = 79.90m
};

Console.WriteLine($"{item.Name}: {item.Stock} units, {item.InventoryValue:F2}");
Console.WriteLine($"sold: {item.TrySell(2)}");
Console.WriteLine($"remaining: {item.Stock}");

public sealed class InventoryItem
{
    public InventoryItem(int openingStock)
    {
        if (openingStock < 0)
            throw new ArgumentOutOfRangeException(nameof(openingStock));
        Stock = openingStock;
    }

    public required string Name { get; init; }
    public decimal UnitPrice { get; init; }
    public int Stock { get; private set; }
    public decimal InventoryValue => UnitPrice * Stock;

    public bool TrySell(int quantity)
    {
        if (quantity <= 0 || quantity > Stock)
            return false;
        Stock -= quantity;
        return true;
    }
}
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

<!-- /quick -->

`Name` must appear in the creation expression, and ordinary assignment can't change it afterward. `UnitPrice` isn't `required`, so omitting it still compiles and produces decimal's default value, `0`. If a real domain rejects a zero price, it needs a construction or validation contract too.

`InventoryValue` always computes from the current `Stock`. The stock has a private setter, but `TrySell` can still use compound assignment inside the type. The method centralizes the quantity range and uses a return value for an expected rejection.

### Validate assignments with `field`

The second example uses C# 14 field-backed properties. Callers still see ordinary properties, while `init` and a private `set` normalize the name and protect the points-mutation entry point.

```csharp
// file: ValidatedProfile.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;

var profile = new CustomerProfile
{
    DisplayName = "  Ada  "
};

profile.AddPoints(25);
Console.WriteLine($"{profile.DisplayName}: {profile.LoyaltyPoints}");

try
{
    profile.AddPoints(-1);
}
catch (ArgumentOutOfRangeException)
{
    Console.WriteLine("negative points rejected");
}

public sealed class CustomerProfile
{
    public required string DisplayName
    {
        get;
        init => field = string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Display name is required")
            : value.Trim();
    }

    public int LoyaltyPoints
    {
        get;
        private set => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value));
    }

    public void AddPoints(int points) => LoyaltyPoints += points;
}
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

`required` on `DisplayName` checks that creation code didn't omit the member. Its `init` body separately rejects whitespace and stores the trimmed value. Neither constraint replaces the other. Even an explicit assignment of `null` satisfies the required-member condition, so runtime validation still matters.

`AddPoints` writes the result back to `LoyaltyPoints`, so the private setter validates the final sum. This call with a negative value is rejected because the result falls below zero. If the business rule forbids a negative `points` argument itself, the method should check that separately at entry.

### Protect collection ownership

The third example keeps its list inside the object and exposes only a read-only wrapper. A read-only property doesn't make an object deeply immutable, so the type still controls collection changes through a method.

```csharp
// file: PurchaseOrder.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

var order = new PurchaseOrder { Number = "PO-42" };
order.AddLine(new OrderLine("Keyboard", 2, 79.90m));
order.AddLine(new OrderLine("Cable", 1, 12.50m));

Console.WriteLine($"{order.Number}: {order.Lines.Count} lines");
Console.WriteLine($"total: {order.Total:F2}");

public sealed class PurchaseOrder
{
    private readonly List<OrderLine> _lines = [];

    public PurchaseOrder()
    {
        Lines = _lines.AsReadOnly();
    }

    public required string Number { get; init; }
    public ReadOnlyCollection<OrderLine> Lines { get; }
    public decimal Total => _lines.Sum(line => line.Quantity * line.UnitPrice);

    public void AddLine(OrderLine line)
    {
        ArgumentNullException.ThrowIfNull(line);
        if (line.Quantity <= 0 || line.UnitPrice < 0)
            throw new ArgumentOutOfRangeException(nameof(line));
        _lines.Add(line);
    }
}

public sealed record OrderLine(string Product, int Quantity, decimal UnitPrice);
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

`Lines` has no setter, so callers can't replace the wrapper. `ReadOnlyCollection` also exposes no add or remove operation. When the internal list changes, the wrapper reflects the latest contents; that is this API's chosen live-view behavior.

The elements are immutable records, preventing a caller from retrieving a line and quietly changing its quantity. If elements were mutable, a read-only collection would protect only the collection structure, not element state. That ownership decision must be explicit in the type's contract.

## Pitfalls

### An accessor recursively calls itself

> **Pitfall:** A generated setter might say `set => Name = value;`, while a getter might say `get => Name;`. Both invoke the same accessor again until a `StackOverflowException` occurs.

**Fix:** write an explicit backing field, or use `field` in C# 14. Search accessor bodies for the property's own name and add a minimal read-write test. Don't treat this failure as an ordinary validation exception that should be caught.

### A getter hides I/O or state changes

> **Pitfall:** `Report` looks like ordinary data but might query a database, read a file, or advance a counter on every read. Debuggers, log templates, and serializers can read it repeatedly, causing extra work or changing the result.

**Fix:** use a method such as `LoadReportAsync()` for work with visible cost, failure modes, or side effects. Keep computed properties predictable. If caching is necessary, state its owner, threading behavior, and invalidation conditions.

### `init` or a private setter is mistaken for deep immutability

> **Pitfall:** `public List<string> Tags { get; init; }` only prevents `Tags` from pointing at another list after construction. A caller can still execute `item.Tags.Add(...)`, so the object's observable state still changes.

**Fix:** expose immutable collections, read-only wrappers, or snapshots, and control the mutability of the elements themselves. Review the full object graph reachable from a property, not just the setter's accessibility.

### `required` is mistaken for a validator

> **Pitfall:** `required string Email` can be assigned `null` explicitly, usually producing a nullability warning instead of a missing-required-member error. Reflection, deserializers, and code compiled with older compilers can also bypass creation-expression checking.

**Fix:** enforce null, format, and cross-field checks inside a trusted boundary. Treat `required` as a caller-experience and compile-time completeness tool, not as runtime data validation or a security boundary.

### State changes before validation finishes

> **Pitfall:** A setter that writes a field and then discovers another failed condition can leave the object in a state the caller didn't expect. Assigning several properties one by one can also violate a cross-field invariant temporarily.

**Fix:** validate and normalize a local candidate before committing it to a field. Use a method or factory for atomic transitions involving several members, checking the complete candidate state before mutating the object.

### Writable properties destabilize hash identity

> **Pitfall:** If `Equals` or `GetHashCode` depends on a writable property, changing it after putting the object in a `Dictionary` or `HashSet` can make the collection unable to find that object. Auto-property syntax gives no hint about this identity risk.

**Fix:** define key identity with stable, immutable values, or remove the object before mutation and add it again afterward. For records, also inspect whether properties participating in value equality refer to mutable objects.

<!-- deep -->

## The metadata shape of a property

C# source presents a property as one member, but the Common Language Runtime executes it through accessor methods. A normal instance getter takes no parameters and returns the property type; a setter takes one parameter of that type and returns `void`. Property metadata associates those methods so reflection, serializers, and binding frameworks can treat them as one logical property.

An auto-property's backing field is an implementation detail. Its name, attributes, and existence shouldn't become an application contract. Reflection code that relies on a compiler-generated field name can easily break when an auto-property becomes an explicitly backed property. Bind persistence and transport to public properties or explicit contracts rather than scanning compiler-generated fields.

Property calls also participate in ordinary member rules. A static property has no instance receiver, a virtual property dispatches according to runtime type, and an interface property requires an implementation to supply compatible accessors. Fields have none of that polymorphic behavior, so describing a property as mere “syntax sugar for a public field” misses important semantics.

### Compound assignment still reads and writes

`account.Balance += amount` isn't a direct mutation of a public storage location. Conceptually, it invokes the getter for the old value, computes a new value, and then invokes the setter to write it back. Logic in either accessor can run and throw.

That is especially risky with side-effecting accessors. One apparently single compound assignment triggers both a read and a write, and threads can interleave between those steps. Property access itself provides no atomicity. Shared counters need a lock, `Interlocked`, or a higher-level synchronization contract rather than reliance on the brevity of `Count++`.

A write-only property can't participate in compound assignment because the operation must read the old value first. A get-only property can't receive the final write. The compiler checks these accessor capabilities at the call site.

## Field-backed properties in C# 14

`field` is a contextual keyword inside a property accessor and denotes the compiler-synthesized backing field for that property. It lets one accessor remain auto-implemented while another adds validation. For example, the getter can be a semicolon while `init` normalizes input before writing `field`.

`field` doesn't make several properties share storage; every field-backed property has its own synthesized field. Nor is it a public name that can be passed to another API. Callers still go through the property. An explicit field is often clearer when several properties coordinate around one value or maintain a cross-member invariant.

Existing members can genuinely be named `field`. Inside an accessor scope, write `@field` to refer to such a member, or the new keyword may change name resolution. Generated code containing that name deserves a focused check after an upgrade to C# 14.

Field-backed properties remove boilerplate without changing accessor-design rules. Validation should still finish before mutation, getters still shouldn't hide expensive work, and exception and threading behavior remain part of the API contract. Shorter syntax doesn't replace behavioral review.

## The boundaries of initialization contracts

The compiler enforces `init` at assignment sites. It provides a shallow assignment restriction: a referenced list, dictionary, or ordinary class remains mutable through its own API. A truly immutable object graph needs immutable member types, or defensive copies on input and immutable views on output.

`required` is also primarily a compile-time protocol. A C# creation expression must set all visible required members, but explicitly assigning a default value still counts as setting one. Nullability analysis may separately warn about `null`; the two diagnostics describe different problems.

A constructor marked `SetsRequiredMembers` tells the compiler that it initializes every required member. The compiler doesn't verify that promise, making the attribute an escape hatch that needs review. When generated code adds it mechanically, omitted members disappear from diagnostics at call sites.

Reflection and some object-construction infrastructure don't necessarily use ordinary C# creation expressions, so `required` isn't a deserialization guarantee. Run end-to-end tests against the serializer and configuration actually in use, and validate invariants as data enters the trusted model. Framework support changes with versions and options; a type design can't infer binding behavior from one keyword.

### Inheritance expands the required-member set

A derived type inherits required members from its base type and can add its own. Code creating the derived instance must satisfy the complete set for the final type. Overriding a required property can't remove its required status.

This affects factories and generic code with a `new()` constraint. A type with required members can't be used directly as the type argument when generic code relies on parameterless `new()` because the generic creation site can't supply an object initializer. A factory must accept initialization data or the type must provide a construction path that establishes the contract explicitly.

Adding `required` during API evolution also creates new work for callers that recompile. It might not alter an existing accessor's binary signature, but it changes source-level construction requirements. Public libraries should evaluate it as a caller-contract change.

## Properties and object invariants

Single-property validation can see only one assignment. If `Start` must precede `End`, exposing two separate setters makes assignment order determine the temporary state and can make either order fail first. A constructor, factory, or `ChangeWindow(start, end)` method can validate the entire candidate pair before committing both values.

Normalization is part of the contract too. A setter that trims a name or changes its case can return text different from what the caller assigned. That may be correct, but it needs tests and documentation. Silently normalizing passwords, tokens, or signature inputs commonly corrupts data, so domain requirements must decide the policy.

After a property throws, the object should generally retain its old state. Compute and validate a local candidate before updating the backing field to give a single-property change a clear commit point. When several fields change, establish the complete new state first, then perform the assignments that can't fail.

Observable invariants also include equality, hashing, and ordering. If those behaviors depend on writable properties, mutation affects collection placement, cache keys, or deduplication. Identity properties should normally remain stable after construction, while state properties shouldn't quietly participate in identity.

### Accessibility and overrides preserve contracts

Accessor accessibility determines what callers can do; it isn't just a documentation hint. A `private set` on a public property isn't an external assignment candidate, so caller code fails to compile. That is more reliable than a “do not modify” comment, but the declaring type still owns every internal write path.

An override of a virtual property must preserve the accessor shape allowed by the base contract. When code reads through a base-class reference, runtime dispatch invokes the derived implementation. A derived getter therefore shouldn't unexpectedly add I/O, side effects, or weaker invariants that violate base-class expectations. Review polymorphic properties with the same substitutability care as polymorphic methods.

Hiding an inherited property differs from overriding it. After a derived type declares a same-named property with `new`, access through the base static type still selects the base property, while access through the derived static type selects the new one. Presenting two sets of state on one object depending on the reference type is usually harder to maintain than a deliberate override or a different name.

An interface specifies an accessor shape, not a backing field. One implementation can store a value while another computes it, but both should satisfy the interface's semantic expectations for cost, failure, and mutability. When callers depend on those expectations, document them on the interface and cover them with contract tests.

### Attributes can target different artifacts

An attribute on a property declaration targets the property metadata by default; it doesn't automatically target the compiler-generated backing field. Use the `field:` attribute target when an auto-property's storage needs the annotation. Choose the corresponding target when an accessor's return value or method needs it instead.

This distinction affects serializers, validators, and reflection tools because different tools inspect different metadata locations. Before adding an attribute, confirm whether its consumer reads properties, fields, or accessors. Source-code proximity alone doesn't determine the target.

An integration test can have the real consumer read a minimal model and confirm that the attribute landed on the intended metadata target.

<!-- /deep -->

[Checkpoint: csharp/properties](https://codewiki.com/csharp/properties/#checkpoint)

## Further reading

- [Properties in the C# programming guide](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties)
- [Auto-implemented properties](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties)
- [The `init` keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init)
- [The `required` modifier](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required)
- [C# language specification: properties](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#157-properties)
