100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Encapsulation and Access Modifiers

Understand how C#'s access modifiers — public, private, protected, internal, and their combinations — let you control visibility and enforce encapsulation boundaries.

Advanced OOPBeginner8 min readJul 9, 2026
Analogies

Encapsulation and Access Modifiers

Encapsulation is the object-oriented principle of bundling data with the logic that operates on it, and restricting direct access to that data from outside the type. In C#, access modifiers are the language mechanism that enforces this: they declare, member by member and type by type, who is allowed to see or use a given piece of code. Good encapsulation means a class's internal representation can change freely as long as its public contract stays stable — callers depend only on what is exposed, not on how it is implemented. C# gives you finer-grained control over visibility than many languages, with six distinct access levels rather than the typical three.

🏏

Cricket analogy: A team's dressing room keeps tactical discussions private from the opposition, exposing only the final batting order publicly — encapsulation lets a team change its game plan internally as long as the lineup announced stays consistent.

The access modifier spectrum

public members are visible to any code that can see the type at all. private (the default for class members) restricts visibility to the containing type only. protected extends visibility to the declaring type and any class that derives from it, anywhere. internal restricts visibility to the current assembly (project/DLL) — useful for implementation details shared across your own codebase but not meant for external consumers. protected internal is the union of the two: accessible either from a derived class anywhere, or from anywhere in the same assembly. private protected (C# 7.2+) is the intersection instead: accessible only from a derived class that is also within the same assembly, which is the most restrictive inheritance-aware modifier.

🏏

Cricket analogy: public is a shot any batsman on any team can play; private is a signature technique only that batsman uses; protected is a family coaching trick passed only to direct successors in that batting lineage; internal is a tactic known only within one franchise's squad; protected internal is known to that squad or any inheriting player elsewhere; private protected is known only to successors who also play for that same franchise.

csharp
public class BankAccount
{
    // Only this class can touch the raw balance.
    private decimal _balance;

    // Public read access, but writes are internal to the class hierarchy logic below.
    public decimal Balance => _balance;

    // Visible to this assembly only — e.g. an internal reconciliation job.
    internal void AdjustForReconciliation(decimal delta) => _balance += delta;

    // Visible to this class and subclasses across any assembly.
    protected void ApplyInterest(decimal rate) => _balance += _balance * rate;

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentOutOfRangeException(nameof(amount));
        _balance += amount;
    }

    public bool TryWithdraw(decimal amount)
    {
        if (amount <= 0 || amount > _balance)
            return false;
        _balance -= amount;
        return true;
    }
}

Why encapsulate at all

Exposing _balance as a public field would let any caller set it to a negative number or bypass business rules entirely. By keeping the field private and exposing controlled operations (Deposit, TryWithdraw) plus a read-only computed property, the class guarantees its own invariants hold no matter who calls it. This is the core value of encapsulation: it moves validation and consistency logic into one place, rather than trusting every caller to do the right thing.

🏏

Cricket analogy: A scoreboard operator doesn't let spectators directly edit the run total; they must go through the official scorer who validates each run against what actually happened, preventing a fan from typing in a fake score.

C#'s default accessibility differs by context: class members default to private if unspecified, but top-level type declarations (classes, interfaces, structs) default to internal when no modifier is written. This trips up developers coming from Java, where the no-modifier default is package-private, a different and slightly broader concept than C#'s assembly-scoped internal.

A frequent design mistake is exposing a mutable collection through a public property, e.g. public List<Order> Orders { get; }. Even though the property itself has no setter, callers can still call account.Orders.Add(...) or .Clear() directly, completely bypassing any validation logic. Prefer exposing IReadOnlyList<T> or IReadOnlyCollection<T> and offering explicit AddOrder/RemoveOrder methods that enforce invariants.

File-scoped and other modern visibility tools

C# 11 introduced the file access modifier, which restricts a type's visibility to the single source file it is declared in — useful for source generators and internal helper types that should never leak even within the same assembly. Combined with nested classes (which can use any access modifier, including private), and partial classes for splitting large types across files, C# offers substantial flexibility for shaping exactly how much of a type's surface area is visible to each layer of a codebase.

🏏

Cricket analogy: A file-scoped tactic is like a play the team only discusses in the dressing room and never writes into the shared team manual, invisible even to other coaches on the same franchise staff; a nested specialist role like a designated 'super-sub' exists only within one squad's structure; splitting a long strategy document across multiple notebooks that combine into one plan mirrors partial classes.

  • Encapsulation bundles data with the logic that maintains its invariants, hiding raw internal state from callers.
  • C# has six access levels: public, private, protected, internal, protected internal, and private protected.
  • Class members default to private; top-level type declarations default to internal when unspecified.
  • protected internal is a union (derived-class-anywhere OR same-assembly); private protected is an intersection (derived-class AND same-assembly).
  • Exposing a mutable collection via a public getter-only property still lets callers mutate it directly — prefer read-only interfaces.
  • The file modifier (C# 11+) scopes a type's visibility to a single source file.

Practice what you learned

Was this page helpful?

Topics covered

#CStudyNotes#Programming#EncapsulationAndAccessModifiers#Encapsulation#Access#Modifiers#Modifier#OOP#StudyNotes#SkillVeris