C# Property Usage: Patterns and Pitfalls
c# property usage: Learn practical C# property usage: auto-implemented properties, computed values, validation, performance costs, and when to prefer methods.
Properties are the primary way to expose state on a C# class, but their behavior is often taken for granted until something goes wrong. Misusing properties can lead to hidden side effects, performance surprises, or inconsistent state. This article covers practical C# property usage: when to use auto-implemented properties, how to handle validation, and where properties should be replaced by methods.
Properties vs Fields: When to Use Each
A field is a simple storage location. A property is a pair of accessor methods that control how that storage is read and written. The compiler treats a property as get_ and set_ methods, which means you can add logic without changing the public contract. This is the core reason to prefer properties over public fields.
public class Customer { public string Name; // field – no control public string Email { get; set; } // property – can add validation later }
If you expose a public field, callers can assign any value directly. Later, if you need to enforce a non-empty name or raise an event on change, you must break the API by converting the field to a property. With a property, you can evolve the implementation without affecting callers. Use properties for any member that is part of your public API, even if the current implementation is trivial.
Auto-Implemented Properties and Their Limits
Auto-implemented properties give you a property with a hidden backing field. They are concise and work well when no extra logic is needed in the accessors.
public class Order { public int Id { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; }
The compiler generates a private backing field and simple getter/setter bodies. You can initialize the property at declaration or in the constructor. However, auto-implemented properties have limitations:
- You cannot add validation or side effects without converting to a full property with an explicit backing field.
- You cannot make the getter and setter have different access modifiers (e.g.,
private setis allowed, butprivate getis not). - You cannot use them in interfaces or abstract classes to enforce a particular implementation; they only define the signature.
When you need validation or a computed value, you need an explicit backing field.
Computed Properties and Expression-Bodied Members
A computed property returns a value derived from other state. It has no setter (or a private setter) and is recalculated on each access. Expression-bodied members provide a concise syntax for read-only properties.
public class Rectangle { public double Width { get; set; } public double Height { get; set; } public double Area => Width * Height; public bool IsSquare => Width == Height; }
These properties are evaluated every time they are accessed. If the calculation is expensive, consider caching the result, but be aware that the cache can become stale if the underlying state changes. For simple arithmetic or string formatting, expression-bodied properties are idiomatic and readable.
Validation and Side Effects in Property Setters
When you need to enforce invariants, a property setter is the right place. The setter can validate the incoming value, throw an exception, or trigger a side effect like an event.
public class Account { private decimal _balance; public decimal Balance { get => _balance; set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "Balance cannot be negative."); _balance = value; } } }
Validation in the setter keeps the class consistent. It also centralizes the rule so callers don't have to remember to check before assigning. However, avoid throwing exceptions from getters. A getter should be side-effect free and should not depend on mutable state that can cause it to throw unexpectedly. If a getter can fail, consider exposing a method like TryGetValue instead.
Performance Considerations: Property Access Costs
Property access is not free, though the JIT compiler often inlines simple getters and setters. For auto-implemented properties, the cost is essentially the same as a field access after inlining. But when a property contains complex logic, every access repeats that logic. This can matter in hot loops.
public class OrderLine { public decimal UnitPrice { get; set; } public int Quantity { get; set; } public decimal Total => UnitPrice * Quantity; // recomputed each time }
If you access Total thousands of times in a loop, the multiplication is trivial, but if the calculation involves database lookups or heavy parsing, caching becomes important. Also, virtual properties prevent inlining because the JIT cannot know the target at compile time. Mark properties as sealed or non-virtual when performance is critical and inheritance is not needed.
Common Mistakes and How to Avoid Them
One common mistake is using a public field instead of a property. Another is putting expensive or failure-prone logic in a getter. A third is using a property where a method is more appropriate because the operation is not a simple read.
// Avoid: property that performs a database query public List<Order> Orders => _db.GetOrders(); // Prefer: method that signals it may be expensive public List<Order> GetOrders() => _db.GetOrders();
Properties should represent attributes of an object, not operations. If accessing the member triggers a significant computation or has observable side effects, a method is clearer. The .NET design guidelines recommend using a method when the operation is more expensive than a field access or when it returns a different value each time it is called.
Choosing Between Properties and Methods
Use a property when:
- The value is a logical attribute of the object, like
Name,Length, orIsValid. - The getter is cheap and side-effect free.
- The setter is used to enforce invariants or notify changes.
Use a method when:
- The operation is expensive, like a database query or a complex calculation.
- The operation changes state, like
Save()orClear(). - The operation may fail and requires a
Trypattern.
For example, DateTime.Now is a property because it returns a simple value, but List<T>.Sort() is a method because it mutates the list. Following this distinction keeps your API predictable and avoids surprising callers with hidden work.
Property Initialization and Immutability
C# 6 introduced property initializers, and C# 9 introduced init-only setters. An init-only setter allows a property to be set during object initialization but not afterward, enabling immutable objects without constructor boilerplate.
public class Product { public string Name { get; init; } public decimal Price { get; init; } } var product = new Product { Name = "Laptop", Price = 999.99m }; // product.Name = "Phone"; // compile error
This pattern is useful for DTOs, configuration objects, and value types where immutability reduces bugs. For mutable state, a regular setter is fine. The choice depends on whether the object's state should change after creation.
Where Properties Break: Serialization and Reflection
Properties are central to serialization frameworks like System.Text.Json and Newtonsoft.Json. They expect public properties with getters and setters. If you use init-only properties, many serializers can still set them via the constructor or by reflection, but behavior varies. Similarly, ORMs like Entity Framework map properties to columns. If you use a computed property without a setter, it will not be persisted.
When designing classes for serialization, ensure that properties have appropriate accessors and that validation in setters does not reject valid deserialized values. For example, a setter that throws on null might break deserialization if the serializer passes null for a missing field. Consider using nullable types or custom converters when needed.
Final Technical Consideration: Property Caching with Lazy<T>
When a computed property is expensive and the underlying state rarely changes, you can cache the result using Lazy<T>. This avoids recomputation while keeping the property syntax.
public class Report { private Lazy<string> _summary; public Report() { _summary = new Lazy<string>(() => GenerateSummary()); } public string Summary => _summary.Value; private string GenerateSummary() { // expensive operation return "..."; } }
The first access triggers the calculation, and subsequent accesses return the cached value. This is a good tradeoff when the cost is high and the data is effectively immutable. However, if the underlying data changes, you must reset the Lazy<T> or use a different caching strategy. This pattern is a pragmatic way to keep the property interface while controlling performance.