.NET Properties
Introduction
Properties are a fundamental feature in .NET that provide a flexible mechanism to read, write, or compute the values of private fields. They enable controlled access to class state while hiding implementation details, which is a key principle of encapsulation in object-oriented programming.
Properties in .NET allow you to expose class members in a way that appears like regular fields to the outside world, but they can contain additional logic such as validation, calculated values, and varying access levels.
Basic Property Syntax
A property in .NET consists of:
- A private field (also called a backing field)
- A getter method (to read the value)
- A setter method (to assign a value)
Here's the basic syntax:
private int _myField; // backing field
public int MyProperty
{
get { return _myField; } // getter
set { _myField = value; } // setter
}
The value
keyword in the setter represents the value being assigned to the property.
Auto-Implemented Properties
For simple properties that don't require additional logic, C# offers auto-implemented (or auto) properties, which simplify the syntax by having the compiler generate the backing field automatically:
public int MyProperty { get; set; }
This is equivalent to the longer form but requires less code.
Property Access Modifiers
Properties can have different access levels for getters and setters:
public int Age
{
get; // public getter
private set; // private setter - can only be set within the class
}
Common combinations include:
public get; private set;
- Read from anywhere, set only within the classpublic get; protected set;
- Read from anywhere, set only within the class or derived classesprivate get; private set;
- Used with expression-bodied properties or calculated properties
Property Usage Example
Let's create a Person
class with properties to understand how they work:
using System;
public class Person
{
// Backing field
private string _name;
// Property with validation logic
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
// Auto-implemented property
public DateTime DateOfBirth { get; set; }
// Read-only calculated property
public int Age
{
get
{
return DateTime.Today.Year - DateOfBirth.Year -
(DateTime.Today.DayOfYear < DateOfBirth.DayOfYear ? 1 : 0);
}
}
// Property with different access modifiers
public string Email { get; private set; }
public Person(string email)
{
Email = email;
}
// Method to update email (since the setter is private)
public void UpdateEmail(string newEmail)
{
if (newEmail.Contains("@"))
Email = newEmail;
}
}
Here's how we can use this class:
// Create a new Person
Person person = new Person("[email protected]");
// Setting properties
person.Name = "John Doe";
person.DateOfBirth = new DateTime(1985, 5, 10);
// Reading properties
Console.WriteLine($"Name: {person.Name}");
Console.WriteLine($"DoB: {person.DateOfBirth.ToShortDateString()}");
Console.WriteLine($"Age: {person.Age}");
Console.WriteLine($"Email: {person.Email}");
// Update email using method (since setter is private)
person.UpdateEmail("[email protected]");
Console.WriteLine($"Updated email: {person.Email}");
// This would cause an error:
// person.Email = "[email protected]"; // Won't compile - setter is private
Output:
Name: John Doe
DoB: 5/10/1985
Age: 38 (value will vary based on current date)
Email: [email protected]
Updated email: [email protected]
Expression-Bodied Properties
For simple computed properties, C# 6.0 introduced expression-bodied properties:
public string FullName => $"{FirstName} {LastName}";
This is shorthand for a read-only property with a getter only.
Init-Only Properties
C# 9.0 introduced init-only properties, which can only be set during object initialization:
public string ID { get; init; }
Usage:
var user = new User { ID = "ABC123" }; // Valid
// user.ID = "XYZ789"; // Invalid - would cause compiler error
Properties vs Fields
Using properties instead of public fields offers several advantages:
- Encapsulation: Properties can hide implementation details.
- Validation: You can validate values before assigning them.
- Calculated Values: Properties can be calculated on demand.
- Debugging: You can set breakpoints in property accessors.
- Flexibility: You can change implementation without affecting the interface.
Practical Example: Banking Application
Let's see a practical example with a simple bank account class:
public class BankAccount
{
// Backing fields
private decimal _balance;
private string _accountNumber;
// Properties
public string AccountNumber
{
get { return _accountNumber; }
private set { _accountNumber = value; }
}
public decimal Balance
{
get { return _balance; }
private set { _balance = value; }
}
public string AccountOwner { get; set; }
public bool IsOverdrawn => Balance < 0;
// Constructor
public BankAccount(string accountNumber, string owner, decimal initialDeposit)
{
AccountNumber = accountNumber;
AccountOwner = owner;
Balance = initialDeposit;
}
// Methods that modify the balance
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
Balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (amount > Balance)
return false;
Balance -= amount;
return true;
}
}
Using this class:
BankAccount account = new BankAccount("123456789", "Alice Smith", 1000m);
// Deposit money
account.Deposit(500m);
Console.WriteLine($"Balance after deposit: {account.Balance}");
// Withdraw money
bool withdrawalSuccess = account.Withdraw(300m);
Console.WriteLine($"Withdrawal {(withdrawalSuccess ? "succeeded" : "failed")}");
Console.WriteLine($"Current balance: {account.Balance}");
// Try to withdraw too much
withdrawalSuccess = account.Withdraw(2000m);
Console.WriteLine($"Excessive withdrawal {(withdrawalSuccess ? "succeeded" : "failed")}");
Console.WriteLine($"Final balance: {account.Balance}");
Console.WriteLine($"Account is {(account.IsOverdrawn ? "overdrawn" : "in good standing")}");
Output:
Balance after deposit: 1500
Withdrawal succeeded
Current balance: 1200
Excessive withdrawal failed
Final balance: 1200
Account is in good standing
Summary
Properties in .NET provide a powerful mechanism for encapsulating class fields while offering controlled access to them. Key points to remember:
- Properties combine the accessibility of fields with the control of methods
- Use auto-implemented properties for simple storage
- Include validation logic in setters for data integrity
- Use different access modifiers to control how properties are accessed and modified
- Calculated properties let you expose derived values
- Init-only properties allow immutability after initialization
By mastering properties, you'll write more maintainable, secure, and professional C# code, adhering to object-oriented best practices.
Additional Resources
Exercises
-
Create a
Rectangle
class withWidth
andHeight
properties that include validation (no negative values), and anArea
calculated property. -
Modify a
Student
class to include properties forName
,ID
(read-only after construction), andGradeAverage
(calculated from a private list of grades). -
Build a
Product
class for an e-commerce system with properties for price, name, and stock level, with appropriate encapsulation and validation.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)