C# Properties

Smart way to access and control class fields

🎛️ What are Properties?

Properties provide a flexible mechanism to read, write, or compute values of private fields. They use get and set accessors to control how fields are accessed and modified.


class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
                                    

Property Types

Auto-Implemented

Quick property declaration

public string Name 
{ 
    get; 
    set; 
}
🎯

Full Property

Complete control with backing field

private int age;
public int Age
{
    get { return age; }
    set { age = value; }
}
📖

Read-Only

Only get accessor, no set

public string FullName
{
    get { return firstName + " " + lastName; }
}
✍️

Write-Only

Only set accessor, no get

public string Password
{
    set { password = value; }
}

🔹 Auto-Implemented Properties

Auto-implemented properties provide a concise syntax for properties that don't require custom logic in their accessors. The compiler automatically generates a private backing field. For example, public string Name { get; set; }. This feature reduces boilerplate code, improves readability, and is ideal for simple data carriers like DTOs (Data Transfer Objects) or model classes where validation isn't immediately needed.

class Student
{
    // Auto-implemented properties
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    
    // Property with default value
    public string Grade { get; set; } = "A";
    
    public void DisplayInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
        Console.WriteLine("Email: " + Email);
        Console.WriteLine("Grade: " + Grade);
    }
}

// Usage
Student student = new Student();
student.Name = "Alice";
student.Age = 20;
student.Email = "[email protected]";

student.DisplayInfo();

// Using object initializer
Student student2 = new Student
{
    Name = "Bob",
    Age = 22,
    Email = "[email protected]",
    Grade = "B"
};

student2.DisplayInfo();

Output:

Name: Alice

Age: 20

Email: [email protected]

Grade: A

Name: Bob

Age: 22

Email: [email protected]

Grade: B

🔹 Full Properties with Validation

Full properties explicitly define get and set accessors, allowing complete control over data assignment and retrieval. You can embed validation, raise events, or perform calculations within the accessors. For instance, preventing negative values in a Balance property. This approach enforces business rules directly within the property, ensuring data integrity and encapsulating validation logic where it belongs—inside the object.

class BankAccount
{
    private decimal balance;
    private string accountNumber;
    
    // Property with validation in setter
    public decimal Balance
    {
        get 
        { 
            return balance; 
        }
        set
        {
            if (value >= 0)
            {
                balance = value;
            }
            else
            {
                Console.WriteLine("Balance cannot be negative!");
            }
        }
    }
    
    // Property with formatting
    public string AccountNumber
    {
        get 
        { 
            return accountNumber; 
        }
        set
        {
            if (value.Length == 6)
            {
                accountNumber = value;
            }
            else
            {
                Console.WriteLine("Account number must be 6 digits!");
            }
        }
    }
}

// Usage
BankAccount account = new BankAccount();

account.AccountNumber = "123456";
account.Balance = 1000;

Console.WriteLine("Account: " + account.AccountNumber);
Console.WriteLine("Balance: $" + account.Balance);

// Try invalid values
account.Balance = -500;
account.AccountNumber = "12";

Output:

Account: 123456

Balance: $1000

Balance cannot be negative!

Account number must be 6 digits!

🔹 Read-Only Properties

Read-only properties in C# are defined with only a get accessor and no set. They are ideal for exposing data that should not be modified after object initialization, such as unique identifiers or calculated values that depend on other fields. For instance, public string EmployeeId { get; private set; } ensures the ID remains constant. This enforces encapsulation and prevents unintended changes to critical data, promoting data integrity and safer code.

class Person
{
    private string firstName;
    private string lastName;
    private DateTime birthDate;
    
    public Person(string first, string last, DateTime birth)
    {
        firstName = first;
        lastName = last;
        birthDate = birth;
    }
    
    // Read-only property (computed value)
    public string FullName
    {
        get { return firstName + " " + lastName; }
    }
    
    // Read-only property with calculation
    public int Age
    {
        get 
        { 
            int age = DateTime.Now.Year - birthDate.Year;
            if (DateTime.Now.DayOfYear < birthDate.DayOfYear)
                age--;
            return age;
        }
    }
    
    // Auto-implemented read-only property (C# 6.0+)
    public string Id { get; } = "EMP001";
}

// Usage
Person person = new Person("John", "Doe", new DateTime(1990, 5, 15));

Console.WriteLine("Full Name: " + person.FullName);
Console.WriteLine("Age: " + person.Age);
Console.WriteLine("ID: " + person.Id);

// Cannot set read-only properties
// person.FullName = "Jane Doe";  // ERROR!
// person.Age = 25;               // ERROR!

Output:

Full Name: John Doe

Age: 34

ID: EMP001

🔹 Properties with Different Access Levels

Properties can have different access modifiers for their get and set accessors, allowing flexible control over data visibility and mutability. A common pattern is a public getter with a private or protected setter, enabling read access from anywhere while restricting write operations to the containing class or derived classes. This is essential for implementing immutable public interfaces, safeguarding internal state, and adhering to the principle of least privilege in object-oriented design.

class Product
{
    // Public get, private set
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    
    // Public get, protected set
    public int Stock { get; protected set; }
    
    // Regular auto-property
    public string Category { get; set; }
    
    public Product(string name, decimal price, int stock)
    {
        Name = name;
        Price = price;
        Stock = stock;
    }
    
    // Public method to modify private setter
    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice > 0)
        {
            Price = newPrice;
            Console.WriteLine("Price updated to $" + Price);
        }
    }
    
    public void Sell(int quantity)
    {
        if (quantity <= Stock)
        {
            Stock -= quantity;
            Console.WriteLine("Sold " + quantity + " units");
        }
    }
}

// Usage
Product product = new Product("Laptop", 999.99m, 50);
product.Category = "Electronics";

Console.WriteLine("Product: " + product.Name);
Console.WriteLine("Price: $" + product.Price);
Console.WriteLine("Stock: " + product.Stock);

// Can read but cannot directly set
// product.Name = "Desktop";   // ERROR!
// product.Price = 899.99m;    // ERROR!

// Use public methods to modify
product.UpdatePrice(899.99m);
product.Sell(5);
Console.WriteLine("Remaining stock: " + product.Stock);

Output:

Product: Laptop

Price: $999.99

Stock: 50

Price updated to $899.99

Sold 5 units

Remaining stock: 45

🔹 Expression-Bodied Properties

Expression-bodied properties, introduced in C# 6.0, provide a concise, single-line syntax for properties whose get accessors perform a simple calculation or return an expression. Instead of a full getter block, you use the lambda arrow (=>), like public double Area => Math.PI * Radius * Radius;. This reduces boilerplate code, improves readability for straightforward properties, and is widely used for computed values that do not require additional logic or validation within the property body.

class Circle
{
    public double Radius { get; set; }
    
    // Expression-bodied read-only property
    public double Area => 3.14159 * Radius * Radius;
    
    // Expression-bodied property with get and set (C# 7.0+)
    private double diameter;
    public double Diameter
    {
        get => diameter;
        set => diameter = value;
    }
    
    // Traditional equivalent of Area property
    public double Circumference
    {
        get { return 2 * 3.14159 * Radius; }
    }
}

// Usage
Circle circle = new Circle();
circle.Radius = 5;

Console.WriteLine("Radius: " + circle.Radius);
Console.WriteLine("Area: " + circle.Area);
Console.WriteLine("Circumference: " + circle.Circumference);

circle.Diameter = 10;
Console.WriteLine("Diameter: " + circle.Diameter);

Output:

Radius: 5

Area: 78.53975

Circumference: 31.4159

Diameter: 10

🔹 Properties vs Fields

Properties and fields serve distinct purposes in C#; properties are accessor methods that can include logic, while fields are simple data storage. Properties offer control via getters/setters, enabling validation, event raising, or data binding. Fields should typically be private, exposing data only through properties when necessary. For example, a property can reject invalid values (e.g., negative age), whereas a field cannot. This distinction is crucial for robust, maintainable, and secure application architecture, favoring properties for public data exposure.

class Employee
{
    // Field - direct access, no control
    public string department;
    
    // Property - controlled access
    private int age;
    public int Age
    {
        get { return age; }
        set 
        { 
            if (value >= 18 && value <= 65)
                age = value;
            else
                Console.WriteLine("Invalid age!");
        }
    }
    
    // Property with computed value
    private decimal salary;
    public decimal Salary
    {
        get { return salary; }
        set { salary = value; }
    }
    
    public decimal AnnualSalary
    {
        get { return salary * 12; }
    }
}

// Usage
Employee emp = new Employee();

// Field - no validation
emp.department = "IT";

// Property - with validation
emp.Age = 25;
emp.Age = 150;  // Invalid!

emp.Salary = 5000;
Console.WriteLine("Monthly: $" + emp.Salary);
Console.WriteLine("Annual: $" + emp.AnnualSalary);

Output:

Invalid age!

Monthly: $5000

Annual: $60000

🔹 Property Best Practices

When to Use Properties:

  • Validation: When you need to validate values before storing
  • Computed Values: For calculated or derived values
  • Encapsulation: To hide implementation details
  • Change Notification: When you need to trigger events on value changes
  • Public Interface: Always use properties instead of public fields

Guidelines:

  • Use auto-implemented properties for simple cases
  • Use full properties when you need validation or logic
  • Make properties read-only when values shouldn't change
  • Use private setters to control how values are modified
  • Keep property getters fast and without side effects

🧠 Test Your Knowledge

What keyword is used in a property setter to represent the incoming value?