Fields store data. Properties control how data is accessed and changed.
Imagine you're building a bank account system. You need to store the account balance, but you don't want anyone to be able to set it to a negative value directly. You also want to be notified when the balance changes.
In C#, fields are where you store data, and properties are the gatekeepers that control access to that data. They work together to give you fine-grained control over your object's state.
In this lesson, you'll learn what fields and properties are, how they differ, when to use each, and how to write clean, maintainable code using modern C# features.
A field is a variable that belongs to a class or struct. It's where you store data.
A property is a member that provides a controlled way to access a field. It looks like a field from the outside, but inside it's a pair of methods: a get accessor (read) and a set accessor (write).
Field = a storage location. It holds data directly.
Property = a member that wraps a field (or computes a value) and controls access through get/set methods.
Think of a field as a safe, and a property as the security guard who controls who opens it.
A field is a member of a class or struct that represents a variable of a specific type. It can be static (shared across all instances) or instance (each object has its own copy). It can be readonly (can only be set during construction).
A property is a member that combines a name with get and/or set accessors. It behaves like a field when accessed but executes code when read or written. Properties are the preferred way to expose data from a class because they allow encapsulation, validation, and future flexibility.
| Feature | Field | Property |
|---|---|---|
| Purpose | Store data | Control access to data |
| Can contain logic | No | Yes (get/set can have code) |
| Can validate input | No | Yes (in set accessor) |
| Can compute value | No | Yes (getter can compute) |
Can be readonly |
Yes | Yes (init or no set) |
| Can raise events | No | Yes (in setter) |
| Data binding support | No | Yes (e.g., INotifyPropertyChanged) |
If you make all your fields public, any code can change them arbitrarily. This leads to:
Properties solve these problems by providing a controlled interface to your data:
private; properties control access.INotifyPropertyChanged).Here's how fields and properties work together inside a class:
var b = account.Balance;account.AccountHolder = "Alice";public decimal Interest { get => Balance * 0.05m; }Let's trace how fields and properties are defined and used.
public class Person
{
// Field: stores the actual data
private string _name;
private int _age;
}Fields are usually private to encapsulate data. They hold the actual state.
public class Person
{
private string _name;
private int _age;
// Property: controls access to _name
public string Name
{
get => _name;
set => _name = value;
}
// Property: controls access to _age with validation
public int Age
{
get => _age;
set
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(value), "Age must be between 0 and 150.");
_age = value;
}
}
}The property wraps the field. get returns the value; set validates and assigns.
public class Person
{
// Auto-implemented property: compiler generates a private backing field
public string Name { get; set; }
// Auto-implemented with private setter (read-only outside class)
public int Age { get; private set; }
// Auto-implemented with init (set only during construction)
public string Email { get; init; }
}Auto-implemented properties are the most common form. The compiler generates a backing field automatically.
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
// Computed property: no backing field
public double Area => Width * Height;
// Expression-bodied getter
public double Perimeter => 2 * (Width + Height);
}Computed properties don't store data — they calculate it on the fly from other fields or properties.
public class Person
{
// Required: must be set during object initialization
public required string Name { get; init; }
public required int Age { get; init; }
// Optional: can be omitted
public string? Nickname { get; set; }
}
// Usage
var person = new Person
{
Name = "Alice", // Required — must be set
Age = 30 // Required — must be set
// Nickname is optional
};required ensures that a property is set during initialization, preventing incomplete object creation.
Let's build a BankAccount class that demonstrates fields, properties, validation, and computed values.
public class BankAccount
{
// ─── Fields (private storage) ───
private decimal _balance;
private string _accountHolder;
private DateTime _createdAt;
// ─── Constructor ───
public BankAccount(string accountHolder, decimal initialBalance = 0)
{
if (string.IsNullOrWhiteSpace(accountHolder))
throw new ArgumentException("Account holder is required.", nameof(accountHolder));
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative.", nameof(initialBalance));
_accountHolder = accountHolder;
_balance = initialBalance;
_createdAt = DateTime.UtcNow;
}
// ─── Properties (controlled access) ───
// Read-only: only the class can modify the balance
public decimal Balance
{
get => _balance;
private set
{
if (value < 0)
throw new InvalidOperationException("Balance cannot be negative.");
_balance = value;
}
}
// Read-write with validation
public string AccountHolder
{
get => _accountHolder;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Account holder is required.", nameof(value));
_accountHolder = value;
}
}
// Read-only: computed from creation date
public DateTime CreatedAt => _createdAt;
// Computed property: time since creation
public TimeSpan AccountAge => DateTime.UtcNow - _createdAt;
// Computed property: formatting
public string BalanceFormatted => Balance.ToString("C");
// ─── Methods that modify state ───
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive.", nameof(amount));
Balance += amount; // Uses the private setter
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive.", nameof(amount));
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds.");
Balance -= amount; // Uses the private setter
}
}
// ─── Usage ───
var account = new BankAccount("Alice", 1000m);
Console.WriteLine($"Holder: {account.AccountHolder}"); // Alice
Console.WriteLine($"Balance: {account.BalanceFormatted}"); // $1,000.00
Console.WriteLine($"Created: {account.CreatedAt}");
account.Deposit(250m);
Console.WriteLine($"Balance after deposit: {account.BalanceFormatted}"); // $1,250.00
account.Withdraw(100m);
Console.WriteLine($"Balance after withdrawal: {account.BalanceFormatted}"); // $1,150.00
try
{
account.Withdraw(2000m); // Throws: insufficient funds
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}Code → Meaning → Result
_balance and _accountHolder are private fields — they hold the actual data.Balance has a private set — only methods inside the class can change it.AccountHolder validates that the name isn't null or whitespace.BalanceFormatted is a computed property — it formats the balance as currency.Deposit and Withdraw methods modify the balance through the private setter, ensuring validation.In a real e-commerce application, you'd have a Customer class with fields and properties that handle validation, formatting, and change notification.
using System.ComponentModel;
public class Customer : INotifyPropertyChanged
{
// ─── Backing fields ───
private string _firstName;
private string _lastName;
private string _email;
private DateTime _dateOfBirth;
private bool _isActive;
// ─── Event for change notification (UI binding) ───
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// ─── Properties with validation and notification ───
public string FirstName
{
get => _firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name is required.", nameof(value));
if (_firstName != value)
{
_firstName = value.Trim();
OnPropertyChanged(nameof(FirstName));
OnPropertyChanged(nameof(FullName)); // Derived property changed
}
}
}
public string LastName
{
get => _lastName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Last name is required.", nameof(value));
if (_lastName != value)
{
_lastName = value.Trim();
OnPropertyChanged(nameof(LastName));
OnPropertyChanged(nameof(FullName));
}
}
}
public string Email
{
get => _email;
set
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("Valid email address is required.", nameof(value));
if (_email != value)
{
_email = value.ToLowerInvariant();
OnPropertyChanged(nameof(Email));
}
}
}
public DateTime DateOfBirth
{
get => _dateOfBirth;
set
{
if (value > DateTime.UtcNow)
throw new ArgumentException("Date of birth cannot be in the future.", nameof(value));
if (_dateOfBirth != value)
{
_dateOfBirth = value;
OnPropertyChanged(nameof(DateOfBirth));
OnPropertyChanged(nameof(Age));
}
}
}
public bool IsActive
{
get => _isActive;
set
{
if (_isActive != value)
{
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
}
// ─── Computed properties ───
public string FullName => $"{FirstName} {LastName}";
public int Age
{
get
{
var today = DateTime.UtcNow;
var age = today.Year - DateOfBirth.Year;
if (DateOfBirth.Date > today.AddYears(-age)) age--;
return age;
}
}
// ─── Required properties for object initialization ───
public required string CustomerId { get; init; }
// ─── Constructor ───
public Customer(string firstName, string lastName, string email, DateTime dateOfBirth)
{
FirstName = firstName;
LastName = lastName;
Email = email;
DateOfBirth = dateOfBirth;
IsActive = true;
}
}
// ─── Usage in an application ───
var customer = new Customer("Jane", "Doe", "jane.doe@example.com", new DateTime(1990, 5, 15))
{
CustomerId = "CUST-001"
};
Console.WriteLine($"Customer: {customer.FullName}");
Console.WriteLine($"Email: {customer.Email}");
Console.WriteLine($"Age: {customer.Age}");
Console.WriteLine($"Active: {customer.IsActive}");
// Change a property — triggers PropertyChanged event
customer.FirstName = "Janet";
Console.WriteLine($"Updated name: {customer.FullName}");
// Attempt invalid email
try
{
customer.Email = "invalid-email";
}
catch (ArgumentException ex)
{
Console.WriteLine($"Validation error: {ex.Message}");
}This example demonstrates:
INotifyPropertyChanged for UI binding (WPF, MAUI, Blazor).FullName and Age derived from other properties.CustomerId must be set during initialization.Think of a field as the bank vault where money is stored. It's secure, but you can't just walk in and grab cash.
A property is the bank teller. The teller:
The vault (field) holds the cash. The teller (property) controls who gets it and how.
What actually happens inside the .NET runtime when you define fields and properties?
static fields are stored in a separate type-specific memory area, shared across all instances.get_Name() and set_Name(value).call instructions when you read or assign to a property.<Name>k__BackingField).[SetsRequiredMembers] attribute to constructors that set all required properties.Many beginners think auto-implemented properties are fields. They're not — they're properties with a compiler-generated backing field.
// This is a FIELD (no get/set methods)
public string Name; // Avoid public fields
// This is an AUTO-IMPLEMENTED PROPERTY (compiler generates backing field + get/set)
public string Name { get; set; } // Preferred
// This is a FULL PROPERTY with explicit backing field
private string _name;
public string Name
{
get => _name;
set => _name = value;
}readonly Field vs get-Only Property vs initreadonly field — can only be assigned in the constructor or inline.public int Age { get; } — property with only a getter (no setter). Can be set via constructor.public string Name { get; init; } — can be set during object initialization only.When should you use a property vs a method? Properties should be used for:
Methods should be used for:
Deposit()).Wrong:
public class Person
{
public string Name; // Public field — breaks encapsulation
}Correct:
public class Person
{
public string Name { get; set; } // Auto-implemented property
}Wrong:
public class Order
{
public List<OrderItem> Items { get; set; } // Callers can modify the list directly
}Correct:
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items; // Read-only view
public void AddItem(OrderItem item) => _items.Add(item);
}Wrong (UI doesn't update):
public class ViewModel
{
public string Name { get; set; } // No INotifyPropertyChanged
}Correct:
public class ViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
}Wrong:
public decimal CalculatedValue
{
get
{
// This runs every time the property is accessed!
return ExpensiveDatabaseQuery();
}
}Correct:
private decimal? _cachedValue;
public decimal CalculatedValue
{
get
{
_cachedValue ??= ExpensiveDatabaseQuery();
return _cachedValue.Value;
}
}required when it makes senseWrong:
public class Person
{
public string Name { get; set; } // Could be left null
}Correct:
public class Person
{
public required string Name { get; init; } // Must be set during initialization
}private unless you have a very specific reason.init / requiredINotifyPropertyChanged in UI-bound view models (WPF, MAUI, Blazor).init = "set it once, then it's locked"required = "you must provide this when creating the object"public string Name { get; set; }).get => ...) are great for derived values with no backing field.init and required help enforce immutability and complete object initialization.You've seen how fields store data and properties control access. Let's test your knowledge.
1. What is the primary difference between a field and a property?
Correct: B
Why B is correct: A field is a storage location — a variable that holds data. A property is a member that provides get and/or set accessors — they are methods that control how data is read and written.
Why A is incorrect: Both fields and properties are part of the class definition. Fields are stored in the object's memory layout on the heap (for reference types). The storage location depends on the type, not whether it's a field or property.
Why C is incorrect: Both fields and properties can have any accessibility modifier (public, private, etc.). The recommendation is to keep fields private and properties public, but it's not a fundamental difference.
Why D is incorrect: Fields can be slightly faster because there's no method call overhead. However, the JIT compiler often inlines simple property getters/setters, making the difference negligible in practice.
Reinforcement: Fields hold data. Properties control access to data through get/set methods.
2. What does the required keyword do in C# 11+?
Correct: B
Why B is correct: The required modifier (C# 11) ensures that a property is set during object initialization. If you create an object and don't set a required property, the compiler produces an error. This helps prevent incomplete object states.
Why A is incorrect: That's the purpose of init (C# 9) — it makes a property set-only during initialization. required is about requiring the property to be set, not about immutability after that.
Why C is incorrect: required has nothing to do with thread safety.
Why D is incorrect: A computed property uses get => ... without a backing field. required is about initialization requirements.
Reinforcement: required ensures required data is provided at creation time. init makes it immutable after creation.
3. Which of the following correctly implements a property that validates the age must be between 0 and 150?
Correct: D
Why D is correct: This is a full property with an explicit backing field (_age). The setter validates the input (value >= 0 && value <= 150) and throws an exception if invalid, then assigns the value to the backing field.
Why A is incorrect: This is an auto-implemented property with no validation. Any value can be assigned.
Why B is incorrect: This is an auto-implemented property with a private setter. It prevents external modification but still no validation when set from inside the class.
Why C is incorrect: This is a full property but with no validation in the setter. It's just a simple wrapper around the backing field.
Reinforcement: Use full properties with validation logic in the setter when you need to enforce business rules on data assignment.
4. What is the output of the following code?
public class Rectangle
{
public double Width { get; set; } = 10;
public double Height { get; set; } = 5;
public double Area => Width * Height;
}
var r = new Rectangle();
r.Width = 8;
Console.WriteLine(r.Area); Correct: B — 40
Why B is correct: Area is a computed property (get => Width * Height). It calculates the value on demand. After setting Width = 8, Area returns 8 * 5 = 40.
Why A is incorrect: 50 would be the area if Width were still 10 and Height 5, but Width was changed.
Why C is incorrect: 80 would be 8 * 10 if Height were 10, but it's 5.
Why D is incorrect: 0 would only occur if Width or Height were 0.
Reinforcement: Computed properties recalculate their value every time they're accessed. They have no backing field and are ideal for derived values.
5. Which of the following is a valid reason to use a full property (with a backing field) instead of an auto-implemented property?
Correct: B
Why B is correct: Full properties with explicit backing fields allow you to add logic in the getter and setter. This is essential for validation, change notification, logging, or any other logic that should run when a property is accessed or modified.
Why A is incorrect: Auto-implemented properties are shorter and more readable. Full properties are more verbose — you use them when you need the extra control.
Why C is incorrect: Properties are not automatically thread-safe. You still need synchronization (locks, etc.) if multiple threads access the same property concurrently.
Why D is incorrect: Serialization depends on the serializer and attributes like [JsonIgnore], not on whether it's a full or auto property.
Reinforcement: Use full properties when you need control — validation, events, computed values, or lazy loading. Use auto-properties for simple data.
You now have a solid understanding of fields and properties — how to store data and control access to it in C#!
dotnetmadeeasy.com — Learn C# and .NET, the right way.