Overview
Properties are central to C# object-oriented design. They enable encapsulation by controlling how data is accessed and modified. Understanding property patterns helps create maintainable, robust APIs.
Core Concepts
Field vs Property
public class Example
{
// Field: Direct variable storage
public int publicField = 10; // Bad: No encapsulation
private int _privateField = 10; // OK: Internal use
// Property: Controlled access
public int PublicProperty { get; set; } = 10; // Good: Encapsulated
// Why properties over public fields?
// 1. Can add validation later without breaking API
// 2. Can add logic (logging, notification)
// 3. Can make read-only or write-only
// 4. Works with data binding frameworks
// 5. Can be virtual (override in derived classes)
}
// Adding validation later - property version (non-breaking)
public int Value
{
get => _value;
set => _value = value >= 0 ? value : throw new ArgumentException();
}
Auto-Implemented Properties
public class Product
{
// Simple auto-property
public string Name { get; set; }
// Auto-property with initializer
public decimal Price { get; set; } = 0.0m;
// Auto-property with different access levels
public int Stock { get; internal set; }
// Read-only auto-property (set in constructor only)
public string Sku { get; }
// Init-only property (C# 9+)
public string Category { get; init; }
// Required property (C# 11+)
public required string Manufacturer { get; init; }
public Product(string sku)
{
Sku = sku; // Can only be set here
}
}
// Usage with object initializer
var product = new Product("SKU123")
{
Name = "Widget",
Price = 19.99m,
Category = "Electronics",
Manufacturer = "Acme" // Required
};
// product.Category = "Other"; // Error: init-only
Computed Properties
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
// Expression-bodied computed property (no backing field)
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
public bool IsSquare => Width == Height;
// Full-syntax computed property
public string Description
{
get
{
if (IsSquare)
return $"Square with side {Width}";
return $"Rectangle {Width}x{Height}";
}
}
}
// Usage
var rect = new Rectangle { Width = 10, Height = 5 };
Console.WriteLine(rect.Area); // 50
Console.WriteLine(rect.Perimeter); // 30
Property Validation
public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set
{
if (value < 0)
throw new InvalidOperationException("Balance cannot be negative");
_balance = value;
}
}
private string _accountNumber;
public string AccountNumber
{
get => _accountNumber;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Account number required");
if (value.Length != 10)
throw new ArgumentException("Account number must be 10 digits");
_accountNumber = value;
}
}
// Using validation with init
private string _email;
public string Email
{
get => _email;
init
{
if (!value.Contains('@'))
throw new ArgumentException("Invalid email");
_email = value;
}
}
}
Static and Const
public class Configuration
{
// Const: Compile-time constant, implicitly static
public const string Version = "1.0.0"; // Inlined at compile time
// Static readonly: Runtime constant
public static readonly DateTime StartupTime = DateTime.Now;
// Static property
private static int _instanceCount;
public static int InstanceCount => _instanceCount;
// Static property with backing field
private static string? _connectionString;
public static string ConnectionString
{
get => _connectionString ?? throw new InvalidOperationException("Not configured");
set => _connectionString = value;
}
public Configuration()
{
_instanceCount++;
}
}
// Usage
Console.WriteLine(Configuration.Version); // "1.0.0"
Configuration.ConnectionString = "Server=...";
var config = new Configuration();
Console.WriteLine(Configuration.InstanceCount); // 1
Property Change Notification
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Person : ObservableObject
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _age;
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
}
// Usage (e.g., in WPF/MAUI)
var person = new Person();
person.PropertyChanged += (s, e) =>
Console.WriteLine($"{e.PropertyName} changed");
person.Name = "Alice"; // Triggers: "Name changed"
When to Use vs. When to Avoid
| Scenario | Use It? | Why |
|----------|---------|-----|
| Public API data access | ✅ Property | Encapsulation, can add logic later |
| Private implementation storage | ✅ Field | Backing storage, no need for accessor overhead |
| Compile-time constant | ✅ const field | Inlined by compiler, zero runtime cost |
| Runtime constant | ✅ static readonly | Initialized once, can use runtime values |
| Derived/calculated value | ✅ Computed property | No storage needed, always in sync |
| Expensive computation | ⚠️ Method or cached | Properties should be fast; use GetX() for slow operations |
| Data binding (WPF/MAUI) | ✅ Property with INotifyPropertyChanged | Required for UI updates |
Common Patterns
Lazy Initialization
Defer expensive initialization until first access.
public class Service
{
private ExpensiveObject? _cached;
public ExpensiveObject Expensive => _cached ??= new ExpensiveObject();
}
Validation in Setter
Enforce invariants when values change.
private int _age;
public int Age
{
get => _age;
set => _age = value >= 0 && value <= 150
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
Common Mistakes
Mistake: Public Fields Instead of Properties
// ❌ Bad - public field
public class Person
{
public string Name; // No encapsulation
}
// ✅ Good - auto-property
public class Person
{
public string Name { get; set; } // Can add validation later
}
Why: Public fields can't have logic added later without breaking binary compatibility. Properties allow adding validation, logging, or notification without changing the API.
Mistake: Expensive Computed Property
// ❌ Bad - expensive operation in property
public IEnumerable<Order> Orders => _db.Orders.Where(o => o.UserId == Id).ToList();
// ✅ Good - use method for expensive operations
public IEnumerable<Order> GetOrders() => _db.Orders.Where(o => o.UserId == Id).ToList();
Why: Properties are expected to be fast and side-effect free. Expensive operations should be methods to signal that they do real work.
Practical Example
Scenario: Creating a validated configuration class.
public class DatabaseConfig
{
private string _connectionString = string.Empty;
public string ConnectionString
{
get => _connectionString;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Connection string required");
if (!value.Contains("Server="))
throw new ArgumentException("Invalid connection string format");
_connectionString = value;
}
}
public int MaxPoolSize { get; init; } = 100;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
// Computed property
public bool IsConfigured => !string.IsNullOrEmpty(ConnectionString);
}
Related Topics
- Constructors: Properties are often set via constructors or object initializers
- Access Modifiers: Control property visibility and setter accessibility
- Records: Provide built-in property syntax with value equality
Interview Tips
Tip 1: Always use properties instead of public fields. It's a fundamental encapsulation principle and allows adding logic later without breaking changes.
Tip 2: Know the difference between
{ get; }(read-only, set in constructor) and{ get; private set; }(read-only externally, can change internally).
Tip 3: Understand that init-only properties (C# 9+) are like read-only but can be set in object initializers.
Common Interview Questions
-
What's the difference between a field and a property?
- Field: Direct data storage, typically private. Property: Controlled access via get/set accessors, can include logic. Use properties for public API - enables validation, change notification, and future modifications without breaking changes.
-
When would you use a field instead of a property?
- Private implementation details, const/static readonly values, performance-critical inner loops where property overhead matters (rare), backing fields for properties. Always prefer properties for any externally accessible members.
-
What is an auto-implemented property?
- Property where the compiler generates the backing field automatically. Syntax:
public int X { get; set; }. Reduces boilerplate while maintaining property benefits. Can't add custom logic without converting to full property.
- Property where the compiler generates the backing field automatically. Syntax:
-
What's the difference between readonly field and get-only property?
- Readonly field: Set only in declaration or constructor, accessed directly. Get-only property: No setter, can be computed or backed by readonly field. Property can be virtual, works with interfaces, supports data binding.
-
Explain init-only properties.
- C# 9 feature: properties that can only be set during object initialization (constructor or object initializer). Syntax:
{ get; init; }. Enables immutable objects with convenient initialization syntax.
- C# 9 feature: properties that can only be set during object initialization (constructor or object initializer). Syntax:
-
What is a computed property?
- Property that calculates its value from other data rather than storing it. No backing field. Example:
public double Area => Width * Height;. Recalculated on each access - consider caching for expensive computations.
- Property that calculates its value from other data rather than storing it. No backing field. Example:
-
Can properties be virtual?
- Yes, properties can be virtual, abstract, or override. Fields cannot. This allows derived classes to customize behavior. Common in frameworks for extending functionality.