C# Encapsulation — Public, Private, Internal, Protected Access Modifiers
In this tutorial, you will learn about C# Encapsulation. We cover key concepts, practical examples, and best practices to help you master this topic.
Encapsulation in C# is the principle of bundling data and methods within a class while controlling access through access modifiers, protecting internal state from unintended external modification.
What You'll Learn
You will master encapsulation in C#: the purpose of each access modifier (public, private, internal, protected, private protected, file-scoped), how to design classes with proper encapsulation using properties and methods, the difference between data hiding and security, and best practices for designing robust APIs in .NET.
Why It Matters
Encapsulation is the foundation of maintainable software. Well-encapsulated classes prevent invalid state, reduce coupling between components, and make code easier to refactor. Without proper encapsulation, internal implementation details leak, creating fragile code that breaks when internal logic changes. Enterprise applications depend on encapsulation to enforce business rules and invariants.
Real-World Use
Financial systems encapsulate account balance changes through deposit/withdraw methods that enforce business rules. ASP.NET Core controllers are internally encapsulated behind public API endpoints. Library authors use internal modifiers to hide implementation details. Entity Framework Core entities encapsulate navigation property changes through private setters.
Learning Path
graph LR
A["10: Constructors"] --> B["11: Encapsulation"]
B --> C["12: Inheritance"]
C --> D["13: Polymorphism"]
D --> E["14: Interfaces"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Access Modifiers Reference
| Modifier | Visibility |
|---|---|
public |
No restrictions |
private |
Same class only |
internal |
Same assembly (project) only |
protected |
Same class or derived classes |
protected internal |
Same assembly OR derived classes |
private protected |
Same class OR derived classes in the same assembly |
file (C# 11) |
Same file only |
Access Modifiers in Action
using System;
public class BankAccount
{
// Private: only visible within this class
private decimal _balance;
private List<string> _transactions = new();
private static int _nextAccountNumber = 1000;
// Protected: visible in derived classes but not externally
protected string AccountType { get; }
// Internal: visible within the same assembly
internal int InternalAuditCode { get; set; }
// Public: visible to everyone
public string AccountNumber { get; }
public decimal Balance => _balance;
public BankAccount(decimal initialDeposit)
{
if (initialDeposit < 0)
throw new ArgumentException("Initial deposit cannot be negative");
AccountNumber = $"ACC{_nextAccountNumber++}";
AccountType = "Standard";
_balance = initialDeposit;
_transactions.Add($"Initial deposit: {initialDeposit:C}");
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
_transactions.Add($"Deposit: {amount:C}");
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (amount > _balance) return false;
_balance -= amount;
_transactions.Add($"Withdrawal: {amount:C}");
return true;
}
// Private helper method
private void Log(string message)
{
Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {message}");
}
// Protected method for derived classes
protected IEnumerable<string> GetTransactions() => _transactions.AsReadOnly();
}
Data Hiding with Properties
Properties provide controlled access to fields while maintaining encapsulation:
public class Employee
{
private string _name;
private decimal _salary;
private DateTime _hireDate;
public string Name
{
get => _name;
set => _name = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name cannot be empty")
: value;
}
// Read-only externally, writable internally
public DateTime HireDate => _hireDate;
// Computed property
public int YearsOfService
{
get
{
var years = DateTime.UtcNow.Year - _hireDate.Year;
if (_hireDate.Date > DateTime.UtcNow.AddYears(-years)) years--;
return years;
}
}
// Public method to modify salary (with validation)
public void UpdateSalary(decimal newSalary)
{
if (newSalary < 0)
throw new ArgumentException("Salary cannot be negative");
if (newSalary > _salary * 2 && !IsApprovedByManager())
throw new InvalidOperationException("Large increases require manager approval");
_salary = newSalary;
}
private bool IsApprovedByManager()
{
// Internal validation logic
return true;
}
}
Internal Access and Assembly Structure
The internal modifier restricts visibility to the same assembly:
// File: DataAccess.cs (internal helper)
internal class DatabaseConnection
{
internal string ConnectionString { get; set; }
internal Connection Open() => new Connection(ConnectionString);
}
// File: UserService.cs (public API)
public class UserService
{
public User GetUser(int id)
{
var db = new DatabaseConnection(); // Internal class, visible within assembly
return db.Query<User>($"SELECT * FROM Users WHERE Id = {id}");
}
}
Protected and Inheritance
Protected members are accessible in derived classes but not externally:
public class Animal
{
protected string Name { get; set; }
private int _age;
public Animal(string name)
{
Name = name;
}
protected void Eat() => Console.WriteLine($"{Name} is eating");
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
}
public void Bark()
{
Console.WriteLine($"{Name} says Woof!"); // Name is protected, accessible here
Eat(); // Protected method accessible
// Console.WriteLine(_age); // Error: private
}
}
File-Scoped Type (C# 11)
The file keyword restricts a type to the source file where it is declared:
// File: Helpers.cs
file class FileHelper
{
public static string ReadFile(string path) => File.ReadAllText(path);
}
// File: Program.cs
// Cannot access FileHelper here
Best Practices for Encapsulation
public class Customer
{
// 1. Fields are always private
private string _name;
private List<Order> _orders = new();
// 2. Properties for external access with validation
public string Name
{
get => _name;
private set => _name = value;
}
// 3. Expose read-only views of collections
public IReadOnlyList<Order> Orders => _orders.AsReadOnly();
// 4. Methods for operations with business logic
public void PlaceOrder(Order order)
{
if (order.Total <= 0)
throw new ArgumentException("Order total must be positive");
_orders.Add(order);
}
// 5. Internal methods for implementation details
internal void ApplyLoyaltyDiscount()
{
if (_orders.Count >= 10)
{
// Apply discount logic
}
}
}
The Principle of Least Privilege
Start with the most restrictive access and widen only when necessary:
public class Document
{
// Private by default, widen as needed
private string _content;
private DateTime _lastModified;
// Public API is minimal
public string Content => _content;
public DateTime LastModified => _lastModified;
// Internal for framework-level access
internal void UpdateContent(string content)
{
_content = content;
_lastModified = DateTime.UtcNow;
}
}
Common Mistakes
Mistake 1: Making Everything Public
Exposing all fields as public breaks encapsulation. Any code can modify internal state, making bugs hard to track and Refactoring impossible. Always use properties with controlled setters.
Mistake 2: Returning Mutable References to Internal Collections
// Bad: external code can modify internal list
public List<Order> Orders { get; } = new();
// Good: expose read-only view
private List<Order> _orders = new();
public IReadOnlyList<Order> Orders => _orders;
Mistake 3: Overusing internal When private Suffices
Default to private. Only use internal when you intentionally want visibility within the assembly for testing or framework integration.
Mistake 4: Not Validating in Property Setters
Properties should enforce invariants. A setter that silently accepts invalid data defeats the purpose of encapsulation.
Mistake 5: Exposing Internal State Through Public Methods That Return Internal Types
If a public method returns an internal type, external code cannot use it. Always ensure public methods return public types.
Mistake 6: Using protected When Not Planning Inheritance
Default to private. Only use protected when you specifically design for inheritance. Most classes should be sealed or use Composition Over Inheritance.
Practice Questions
- What is the default access modifier for class members in C#?
- What is the difference between
protectedandprotected internal? - Why should you expose collections as
IReadOnlyList<T>instead ofList<T>? - When would you use the
fileaccess modifier introduced in C# 11? - Design a
Temperatureclass with encapsulation that prevents setting temperature below absolute zero.
Challenge
Design a PasswordManager class that encapsulates a list of passwords. Expose methods to add, validate, and retire passwords. Never expose the actual password list directly. Include a method to check if a password has been used before.
FAQ
Mini Project
Create an encapsulated Library system:
public class Book
{
public string Title { get; }
public string Author { get; }
public string Isbn { get; }
internal Book(string title, string author, string isbn)
{
Title = title;
Author = author;
Isbn = isbn;
}
}
public class Library
{
private List<Book> _books = new();
private HashSet<string> _isbns = new();
private Dictionary<string, string> _borrowedBooks = new();
public IReadOnlyList<Book> Books => _books.AsReadOnly();
public int AvailableBooks => _books.Count - _borrowedBooks.Count;
public Book AddBook(string title, string author, string isbn)
{
if (_isbns.Contains(isbn))
throw new InvalidOperationException("Book with this ISBN already exists");
var book = new Book(title, author, isbn);
_books.Add(book);
_isbns.Add(isbn);
return book;
}
public bool BorrowBook(string isbn, string userId)
{
if (_borrowedBooks.ContainsKey(isbn)) return false;
if (!_isbns.Contains(isbn)) return false;
_borrowedBooks[isbn] = userId;
return true;
}
public bool ReturnBook(string isbn)
{
return _borrowedBooks.Remove(isbn);
}
public IReadOnlyList<string> GetBorrowedByUser(string userId)
{
return _borrowedBooks
.Where(kv => kv.Value == userId)
.Select(kv => kv.Key)
.ToList()
.AsReadOnly();
}
}
var library = new Library();
var book1 = library.AddBook("Clean Code", "Robert Martin", "978-0132350884");
var book2 = library.AddBook("C# in Depth", "Jon Skeet", "978-1617294532");
library.BorrowBook("978-0132350884", "user1");
Console.WriteLine($"Available: {library.AvailableBooks}"); // 1
Console.WriteLine($"Total books: {library.Books.Count}"); // 2
Console.WriteLine($"User1 borrowed: {library.GetBorrowedByUser("user1").Count}"); // 1
library.ReturnBook("978-0132350884");
Console.WriteLine($"Available after return: {library.AvailableBooks}"); // 2
Expected output:
Available: 1
Total books: 2
User1 borrowed: 1
Available after return: 2
What's Next
You have mastered encapsulation and access modifiers in C#. The next lesson covers inheritance: base classes, override, virtual, sealed, and the new keyword.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro