Easy Learn C#

Properties in C#

Introduction to Properties

Properties in C# provide a flexible mechanism to read, write, or compute the values of private fields. They expose fields to the outside world while protecting them from direct access or invalid data.

Why Use Properties?

  • Properties enable controlled access to class fields
  • They allow validation of data before assignment
  • They can compute values on-the-fly
  • They maintain encapsulation while providing a public interface
  • They provide a way to change internal implementation without affecting external code

Basic Property Syntax

Properties consist of a private backing field and public getters and setters that control access to that field.

Full Property Syntax


public class Person
{
    // Private backing field
    private string _name;
    
    // Public property with get and set accessors
    public string Name
    {
        get 
        { 
            return _name; 
        }
        set 
        { 
            _name = value; 
        }
    }
}

// Usage
Person person = new Person();
person.Name = "John";  // Calls the set accessor
string name = person.Name;  // Calls the get accessor

Auto-Implemented Properties

C# provides a simplified syntax for properties that don't require additional logic in their accessors.


public class Person
{
    // Auto-implemented property (compiler generates the backing field)
    public string Name { get; set; }
    
    // Auto-implemented property with default value
    public int Age { get; set; } = 25;
}

// Usage
Person person = new Person();
person.Name = "John";  // The compiler-generated set accessor is called
Console.WriteLine(person.Age);  // Outputs: 25

With auto-implemented properties:

  • The compiler automatically creates a private, anonymous backing field
  • The accessors are implemented without additional logic
  • They provide a concise syntax for simple property definitions
  • You can set default values (C# 6.0 and later)

Property Accessors

Property accessors control how property values are read and written. You can customize them to include validation, notify about changes, or compute values.

Custom Getter and Setter


public class BankAccount
{
    private decimal _balance;
    
    public decimal Balance
    {
        get
        {
            // You could log access or perform calculations here
            Console.WriteLine("Balance was accessed");
            return _balance;
        }
        set
        {
            // Validate the new value before setting it
            if (value < 0)
            {
                throw new ArgumentException("Balance cannot be negative");
            }
            
            _balance = value;
            Console.WriteLine($"Balance updated to {value}");
        }
    }
}

// Usage
BankAccount account = new BankAccount();
account.Balance = 1000;  // Outputs: "Balance updated to 1000"
decimal balance = account.Balance;  // Outputs: "Balance was accessed"

Read-Only and Write-Only Properties


public class User
{
    private string _password;
    
    // Read-only property (only has a getter)
    public string Username { get; }
    
    // Write-only property (only has a setter)
    public string Password
    {
        set
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("Password cannot be empty");
            }
            
            // In a real application, you would hash the password here
            _password = value;
        }
    }
    
    // Constructor sets the read-only property
    public User(string username)
    {
        Username = username;
    }
    
    // Method to verify password without exposing it
    public bool VerifyPassword(string passwordAttempt)
    {
        return _password == passwordAttempt;
    }
}

// Usage
User user = new User("johndoe");
user.Password = "secret123";  // Sets the password
// user.Username = "newname";  // Error: property is read-only
// string password = user.Password;  // Error: property is write-only

Property access modifiers:

  • Read-only property - Only has a getter, can only be set in the constructor or as an initializer
  • Write-only property - Only has a setter, cannot be read directly
  • Computed property - No backing field, calculates value dynamically

Computed Properties

Properties can calculate values on-the-fly rather than storing them in a backing field.

Computed Property Examples


public class Rectangle
{
    // Properties with backing fields
    public double Width { get; set; }
    public double Height { get; set; }
    
    // Computed property - no backing field
    public double Area
    {
        get { return Width * Height; }
    }
    
    // Expression-bodied property (C# 6.0+)
    public double Perimeter => 2 * (Width + Height);
    
    // Computed property that depends on other properties
    public bool IsSquare => Width == Height;
}

// Usage
Rectangle rect = new Rectangle
{
    Width = 5,
    Height = 10
};

Console.WriteLine($"Area: {rect.Area}");       // Outputs: "Area: 50"
Console.WriteLine($"Perimeter: {rect.Perimeter}");  // Outputs: "Perimeter: 30"
Console.WriteLine($"Is Square: {rect.IsSquare}");   // Outputs: "Is Square: False"

rect.Width = 10;
Console.WriteLine($"Is Square: {rect.IsSquare}");   // Outputs: "Is Square: True"

Benefits of computed properties:

  • Avoid storing values that can be calculated from other properties
  • Always return up-to-date values even when dependent properties change
  • Can simplify code by centralizing calculation logic
  • Can be implemented using expression-bodied syntax (=>) for concise one-line properties

Property Access Modifiers

You can control the accessibility of properties and their individual accessors using access modifiers.

Different Access Levels for Get and Set


public class Employee
{
    // Property with public get but private set
    public string Name { get; private set; }
    
    // Property with private get but public set
    public decimal Salary { private get; set; }
    
    // Property with protected set
    public int EmployeeId { get; protected set; }
    
    // Constructor sets the properties
    public Employee(string name, int id)
    {
        Name = name;
        EmployeeId = id;
    }
    
    // Method to access private getter
    public void PrintSalary()
    {
        Console.WriteLine($"Salary: {Salary}");
    }
}

// Derived class
public class Manager : Employee
{
    public Manager(string name, int id) : base(name, id)
    {
        // Can access protected setter
        EmployeeId = 1000 + id;
    }
}

// Usage
Employee employee = new Employee("John", 101);
// employee.Name = "Jane";  // Error: set accessor is private
employee.Salary = 50000;    // Public setter
// decimal salary = employee.Salary;  // Error: get accessor is private
employee.PrintSalary();     // Outputs: "Salary: 50000"

Access modifier combinations:

  • public get; private set; - Everyone can read, only the containing class can write
  • public get; protected set; - Everyone can read, only the class and derived classes can write
  • private get; public set; - Only the class can read, everyone can write (rare)
  • protected get; protected set; - Only the class and derived classes can read and write

Property Patterns and Best Practices

Property Change Notification


public class ViewModel : INotifyPropertyChanged
{
    private string _title;
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    public string Title
    {
        get { return _title; }
        set
        {
            if (_title != value)
            {
                _title = value;
                OnPropertyChanged(nameof(Title));
            }
        }
    }
    
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

This pattern is commonly used in MVVM (Model-View-ViewModel) applications to notify the UI when a property's value changes.

Lazy Loading


public class Document
{
    private string _content;
    private bool _isLoaded;
    private readonly string _filePath;
    
    public Document(string filePath)
    {
        _filePath = filePath;
    }
    
    public string Content
    {
        get
        {
            if (!_isLoaded)
            {
                // Load the content only when first accessed
                _content = File.ReadAllText(_filePath);
                _isLoaded = true;
            }
            
            return _content;
        }
    }
}

This pattern delays loading expensive resources until they're actually needed.

Common Property Design Guidelines

Best Practices for Properties

  • Use properties instead of public fields to maintain encapsulation
  • Keep property logic simple - avoid complex operations in getters
  • Validate data in setters to maintain object invariants
  • Consider immutability - use read-only properties where appropriate
  • Use consistent naming conventions - properties should be PascalCase
  • Make properties thread-safe if they might be accessed concurrently
  • Document property behavior, especially side effects or exceptions
  • Avoid returning references to mutable objects to prevent unexpected modifications

Common Property Mistakes to Avoid

  • Heavy computations in getters without caching
  • Side effects in getters (except for lazy initialization)
  • Not validating inputs in setters
  • Inconsistent access modifiers across related properties
  • Exposing collection properties without read-only wrappers
  • Breaking change notification patterns in UI-bound properties
  • Circular dependencies between properties
  • Mixing property and field naming conventions