← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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 vs Property — The Core Distinction

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.

The Technical Definition

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)

Why Does It Exist?

The Problem

If you make all your fields public, any code can change them arbitrarily. This leads to:

The Solution

Properties solve these problems by providing a controlled interface to your data:

Big Picture

Here's how fields and properties work together inside a class:

FIELDS & PROPERTIES — INSIDE A CLASS
Class: BankAccount
FIELDS (private)
private decimal _balance;
private string _accountHolder;
// ... more fields
PROPERTIES (public)
public decimal Balance { get; private set; }
public string AccountHolder { get; set; }
// ... more properties
Fields store the data (hidden) · Properties control access (public interface)
READ
var b = account.Balance;
→ getter executes
WRITE
account.AccountHolder = "Alice";
→ setter executes (validation, events)
COMPUTED
public decimal Interest { get => Balance * 0.05m; }
→ getter computes, no backing field

How It Works

Let's trace how fields and properties are defined and used.

FIELDS & PROPERTIES — STEP BY STEP
Step 1 — Define a field (the storage)
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.

Step 2 — Define a property (the access control)
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.

Step 3 — Use auto-implemented properties (simplified)
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.

Step 4 — Computed properties (no backing field)
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.

Step 5 — Required properties (C# 11+)
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.

Simple Example

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

Real-World Example

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:

Analogy

Field = Vault, Property = Teller

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.

Under the Hood

What actually happens inside the .NET runtime when you define fields and properties?

INTERNAL VIEW — FIELDS & PROPERTIES
1. FIELDS — Storage Allocation
2. PROPERTIES — Compiled to Methods
3. JIT COMPILATION
4. REQUIRED PROPERTIES (C# 11)

Common Confusion

1. Auto-Implemented Property vs Field

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; }

2. readonly Field vs get-Only Property vs init

3. Property vs Method

When should you use a property vs a method? Properties should be used for:

Methods should be used for:

Common Mistakes

Mistake 1 — Using public fields instead of properties

Wrong:

public class Person { public string Name; // Public field — breaks encapsulation }

Correct:

public class Person { public string Name { get; set; } // Auto-implemented property }

Mistake 2 — Exposing a mutable collection directly

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); }

Mistake 3 — Forgetting to notify changes

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(); } } } }

Mistake 4 — Putting expensive logic in a getter

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; } }

Mistake 5 — Not using required when it makes sense

Wrong:

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 }

When Should I Use It?

Fields
Use for private storage. Always keep them private unless you have a very specific reason.
Auto-Properties
Use for simple data storage with no validation. Most common choice for DTOs and simple classes.
Full Properties
Use when you need validation, change notification, computed values, or logic in get/set.
Computed Properties
Use when the value is derived from other data. No backing field; expression-bodied syntax is clean.
init / required
Use for immutable data that must be set during initialization (C# 9+ / 11+). Great for DTOs, records.
Change Notification
Use INotifyPropertyChanged in UI-bound view models (WPF, MAUI, Blazor).

Mental Model

Field = the storage box (private, holds data)
Property = the gatekeeper (public, controls access)
Auto-property = "I trust the compiler to manage the box"
Full property = "I need to control what goes in and out"
Computed property = "I calculate this on demand, no box needed"
init = "set it once, then it's locked"
required = "you must provide this when creating the object"

Remember:
· Fields are private by default; properties are public by design
· Properties can have logic — validation, events, computation
· Auto-properties are the default choice for simple data
· Use full properties when you need control

Key Takeaway


Check Your Understanding

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?

Show answer

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+?

Show answer

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?

Show answer

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);
Show answer

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?

Show answer

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.