Skip to content

C# Encapsulation — Public, Private, Internal, Protected Access Modifiers

DodaTech Updated 2026-06-28 8 min read

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

  1. What is the default access modifier for class members in C#?
  2. What is the difference between protected and protected internal?
  3. Why should you expose collections as IReadOnlyList<T> instead of List<T>?
  4. When would you use the file access modifier introduced in C# 11?
  5. Design a Temperature class 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

What is the default access modifier in C#?

The default access modifier for class members is private. For top-level types (classes, structs, enums), the default is internal.

Can I change access modifiers on override methods?

An override method must have the same accessibility as the base method. You cannot change access on an override, but you can expose it through another public method.

What is the difference between `private protected` and `protected internal`?

private protected is accessible in the containing class or derived classes within the same assembly. protected internal is accessible in the same assembly or any derived class (even in a different assembly).

Can I access a private method via reflection?

Yes. Reflection can bypass all access modifiers. But this is fragile and should only be used for testing or framework code. Never rely on it in application logic.

Should properties with only getters be auto-properties or expression-bodied?

Both are valid. Auto-properties ({ get; }) are more concise for simple cases. Expression-bodied (=> value) is useful when the getter involves computation.

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