Properties and Fields

Master C# properties and fields including auto-properties, computed properties, and encapsulation best practices

C#
OOP
Properties
Encapsulation
18 min read

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"

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.