Lesson 4: Encapsulation & Access Modifiers in C#
Encapsulation is the first pillar of OOP — the practice of bundling data with methods and hiding internal details. Access modifiers are the tools that enforce encapsulation by controlling which code can access class members. This lesson covers all the access levels and best practices.
The Five Access Modifiers
C# provides five access levels. Here's their scope from most to least restrictive:
| Modifier | Same Class | Same Assembly | Different Assembly | Use Case |
|---|---|---|---|---|
private |
✅ Yes | ❌ No | ❌ No | Hide implementation details |
internal |
✅ Yes | ✅ Yes | ❌ No | Limit visibility to your project |
protected |
✅ Yes | ❌ No | ✅ Yes (via inheritance) | Allow child classes access |
public |
✅ Yes | ✅ Yes | ✅ Yes | Expose official API |
Private — Hide Implementation
Use private for internal fields and methods that shouldn't be accessed from outside the class:
public class Password
{
private string _hash; // Private field — users can't access directly
public Password(string plainText)
{
_hash = HashPassword(plainText); // Validate and hash
}
// Private method — only this class calls it
private string HashPassword(string text)
{
// Hashing logic here
return Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
}
public bool Verify(string plainText)
{
return _hash == HashPassword(plainText);
}
}
// Usage
var pwd = new Password("MySecret123");
pwd.Verify("MySecret123"); // ✓ Works
// pwd._hash = "fake"; // ✗ Compile error — cannot access private
Public — Expose the API
Mark as public only what users of your class need to know:
public class BankAccount
{
public string AccountNumber { get; set; } // Public property
public decimal Balance { get; private set; } // Readable, not writable from outside
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit must be positive");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds");
Balance -= amount;
}
}
// Usage
var account = new BankAccount { AccountNumber = "123456" };
account.Deposit(1000); // ✓ Works
Console.WriteLine(account.Balance); // ✓ Read-only: 1000
// account.Balance = -500; // ✗ Compile error
Protected — Allow Inheritance
Use protected to allow child classes to access members but not external code:
public class Animal
{
protected string Name { get; set; } // Child classes can access
private int Age { get; set; } // Only Animal can access
protected virtual void MakeSound()
{
Console.WriteLine("Some sound");
}
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name} barks!"); // ✓ Access protected Name
}
protected override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
Internal — Project-Wide Access
Use internal for members that should be visible within your assembly but not to external consumers:
internal class ConfigHelper // Only this project can use
{
internal static void LoadSettings() { }
}
public class PublicClass
{
internal int InternalField; // Only this assembly can access
public int PublicField; // Anyone can access
}
💡 Golden Rule: Make things as private as possible, then expand access only when needed. This principle is called the "Principle of Least Privilege."
Properties with Different Access Levels
You can have different access on getters and setters:
public class User
{
// Public getter, private setter
public string Email { get; private set; }
// Public getter, internal setter
public string Username { get; internal set; }
// Private getter, public setter (unusual but possible)
public int Age { private get; set; }
public User(string email)
{
Email = email; // ✓ Internal — can set in constructor
}
public void UpdateEmail(string newEmail)
{
Email = newEmail; // ✓ Can access own private setter
}
}
// Usage
var user = new User("john@example.com");
Console.WriteLine(user.Email); // ✓ Read-only from outside
// user.Email = "jane@example.com"; // ✗ Compile error
🧠 Quick Check — Lesson 4
Which access modifier allows a child class to access a member but hides it from external code?
Lesson Summary
Encapsulation hides internal details and exposes only what's necessary for external use.
private — accessible only within the class; use for implementation details.
protected — accessible to the class and its child classes; use for inherited behavior.
internal — accessible anywhere in the same assembly; use for internal APIs.
public — accessible everywhere; reserve for official, stable APIs.
Apply the Principle of Least Privilege — make everything private by default, then expand access as needed.