AlgoMaster Logo

Access Modifiers

Last Updated: May 17, 2026

10 min read

Access modifiers control who can see and use the members of a type. A Cart class might expose AddItem to anyone but keep its internal item list strictly to itself, and a Discount helper might be visible inside one library but invisible to the apps that depend on it. C# offers six access modifier combinations plus the file keyword from C# 11, and each one answers a slightly different visibility question. This chapter walks through all of them, the defaults that apply when you write nothing, the matrix of where each one is visible, and the rules for which modifiers can appear on which declarations.

Why Visibility Matters

A class is more than a bag of fields and methods. It's a contract with the rest of the codebase. Anything you mark public is something other code can call, depend on, and break if you change. Anything you keep private is yours to refactor freely. The wider the public surface, the harder it gets to evolve the type without breaking callers, so good C# code treats visibility as a budget: spend it only on the members that genuinely need to be reachable from outside.

The other side of the coin is intent. When a reader sees public decimal Total { get; private set; } on a Cart, they immediately know two things: anyone can read the total, but only the cart itself can change it. The visibility tells the story before the body does. Hiding implementation details behind a small public surface is what people mean when they say "information hiding," and access modifiers are the tool that makes it real.

The items list is private, so callers can never reach in and corrupt the running total by adding items behind the cart's back. The Total property has a public getter and a private setter, which means anyone can read it but only the cart's own code can change it. Compare this to a version where everything is public: callers could mutate items directly, the running total would drift out of sync, and any bug in totals would be impossible to pin down.

The Six Access Modifiers

C# has six modifier combinations for type members. Four are single keywords, and two are combinations of protected and internal or private.

ModifierVisible from
publicEverywhere. Same assembly, other assemblies, derived classes, anywhere.
privateOnly inside the same class (or struct) where it's declared.
protectedInside the declaring class and any class that derives from it, in any assembly.
internalAnywhere in the same assembly. Invisible to other assemblies.
protected internalEither condition: same assembly, OR a derived class in any assembly. The union.
private protectedBoth conditions: same assembly AND a derived class. The intersection. (C# 7.2+)

The two combined modifiers are easy to mix up. protected internal is the broader of the two: anything in the same assembly can see it, plus any derived class anywhere. private protected is the narrower of the two: only derived classes inside the same assembly. The names look counterintuitive until you realize C# reads them as "protected (and also) internal" (union) versus "private to subclasses, but only within this assembly" (intersection).

An assembly is the compiled output of one C# project: usually a .dll file (for libraries) or a .exe file (for apps). When you build one project, you get one assembly. The "same assembly" rule lines up with "the same project" for most everyday code.

Main lives in the same assembly as Product, so the internal and protected internal members are reachable. The private and protected ones aren't, because Main is not inside Product and not a subclass. The private protected member needs both conditions, so even though Main is in the same assembly, it can't see InternalSku because it isn't a subclass.

A flowchart helps visualize the decision the compiler runs at every access:

The two questions that gate every access are "is this the same assembly?" and "is this a subclass?" The six modifiers are just different combinations of those two answers.

Default Access Levels

If you write no modifier at all, C# applies a default. The default depends on what kind of declaration it is.

DeclarationDefault
Top-level type (class, struct, record, interface, enum, delegate)internal
Nested type (inside another class)private
Class member (field, method, property, constructor, event)private
Struct memberprivate
Interface memberpublic (you cannot make it less than public)
Enum memberpublic (enum values are always public)

The default for class members is private, which is exactly what you want for fields and helper methods most of the time. The default for top-level types is internal, which means a class with no modifier is visible inside your project but invisible to other projects that reference it. This matches the principle of "smallest surface that works."

Order itself is internal, so any other code inside this project can use it but a separate referenced project cannot. The field total and the method RecalculateTotal are both private by default, so they're invisible outside Order. You only have to type public for the members you actually want to expose, which keeps the visible surface intentional.

A common source of confusion is leaving the modifier off and assuming "no modifier" means "public." It doesn't. The C# defaults are deliberately conservative, you have to opt into wider visibility. If a beginner writes a method without public and then complains it can't be called from Main, this default is the reason.

Where Each Modifier Is Allowed

Not every modifier can appear on every declaration. The rules look fussy, but they follow from what each modifier means.

DeclarationAllowed modifiers
Top-level class, struct, record, enum, delegatepublic, internal, file (C# 11)
Nested type (inside another class or struct)All six modifiers
Class members (field, method, property, event, constructor)All six modifiers
Struct membersAll six modifiers except protected, protected internal, private protected (structs can't be inherited from)
Interface memberspublic (default and only option in classic interfaces)
Enum memberspublic (implicit, you cannot write a modifier)

The two restrictions that matter most:

  1. Top-level types can only be public or internal. They cannot be private or protected. There's no enclosing scope for those modifiers to refer to, a top-level type has no "outer class" to be private to.
  2. Struct members cannot be protected in any form, because structs are implicitly sealed. There's no way to derive from a struct, so a protected member would never be reachable.

CustomerProfile is public, so any project can use it. Its nested History class is private, so it's an implementation detail invisible outside CustomerProfile. InternalAuditLog is internal, so it can be used inside this assembly but is invisible to a separate referencing project. Each modifier matches the role of the type: public API for CustomerProfile, internal helper for InternalAuditLog, private implementation for History.

Nested types are the place where the full six-modifier toolbox actually pays off. A nested type can be private to its enclosing class (used only inside), protected to subclasses, or internal to the assembly. Top-level types can't express that range because there's no enclosing scope for private or protected to refer to.

Visibility Matrix

The six modifiers come down to a simple grid of "yes / no" answers in four scenarios. Memorize this table and most access questions become a lookup.

ModifierSame classDerived class<br/>(same assembly)Other class<br/>(same assembly)Derived class<br/>(other assembly)Other class<br/>(other assembly)
publicYesYesYesYesYes
protected internalYesYesYesYesNo
internalYesYesYesNoNo
protectedYesYesNoYesNo
private protectedYesYesNoNoNo
privateYesNoNoNoNo

Reading rows top to bottom, the visibility narrows from "everyone everywhere" down to "only the declaring class itself." Reading the columns left to right, you're moving farther away from the declaring class: from inside, to a subclass, to an unrelated class, and then crossing into a different assembly.

The two intuition checks for the combined modifiers:

  • protected internal is wider than either protected or internal on its own. It's a union: "in the same assembly OR in a subclass."
  • private protected is narrower than either. It's an intersection: "in the same assembly AND in a subclass."

A short example that makes the difference concrete. Suppose we have a base class Discount in one assembly, and a derived class SeasonalDiscount in a different assembly.

SeasonalDiscount is a subclass of Discount, so protected and protected internal members are reachable. It's in a different assembly, though, so private protected (which requires both subclass relationship and same assembly) is not.

The file Modifier (C# 11)

C# 11 introduced the file modifier as a new visibility level even narrower than private. A type marked file is visible only within the single source file where it's declared. From the perspective of any other file, even files in the same project, the type doesn't exist.

The Cart class is public and reachable from Program.cs. The helper CartIdGenerator is file-scoped, so it's invisible outside CartHelpers.cs even though both files are part of the same project. Each file gets its own "name space" for file-scoped types, which means two files can each declare a file class CartIdGenerator without colliding.

The file modifier was added primarily for source generators, which often emit helper types alongside user code and need to avoid name clashes. It also has hand-written uses: a small helper class that you want bound to one source file, with no chance of being used elsewhere by accident.

Rules:

  • Only allowed on top-level types (class, struct, record, interface, enum, delegate).
  • Cannot be combined with other access modifiers (public file class is not allowed).
  • Members of a file type follow normal rules (default private, can be public to reach within the file, etc.).

This is a niche feature compared to the main six modifiers. Reach for it when you genuinely want a type bound to one file, otherwise stick with private for nested types and internal for top-level helpers.

Choosing the Right Modifier

A practical heuristic: pick the narrowest modifier that compiles. Start with private, widen to protected or internal only if a real caller needs the access, and reserve public for members that are part of the type's intended API.

QuestionChoose
Only the declaring class uses itprivate
Subclasses also need it, same assembly onlyprivate protected
Anything in the assembly needs itinternal
Subclasses anywhere need itprotected
Same assembly OR any subclass anywhereprotected internal
External callers need itpublic
Bound to one source file (helpers, generators)file

Here's a longer e-commerce example that exercises most of the modifiers in a believable way.

Each modifier has a job. CustomerName and Total are public because they're the type's external contract. charges is private because it's the raw storage backing the public Total. BaseDiscount is protected because subclasses need to set it but external code shouldn't. OrderId is internal because the database-mapping code in the same project needs it, but external apps that reference this library shouldn't. The whole design is "smallest visibility that works."

A confusing variant is the asymmetric property accessor. You've seen this with public decimal Total { get; private set; } earlier in the lesson. The getter and setter can have different access levels, as long as one of them matches the property's overall declared modifier.

StockCount is publicly readable, but only Inventory.Restock can change it. The private on the setter is the asymmetry: the property is declared public, and the setter is restricted further. This is the standard pattern for "read-only to the outside, writable inside."

Common Mistakes

A short tour of the access-modifier traps that come up most often.

Marking everything public. New C# developers sometimes mark every field and method public so the program compiles. The code runs, but the type's contract is now "everything," and any caller can corrupt state by writing to fields directly. Prefer private fields with public properties or methods.

Confusing `internal` and `private`. internal means "visible inside this assembly." private means "visible inside this type only." A common bug is marking a helper class private (which works only for nested types) and getting a confusing compile error, or marking it internal and being surprised when a different project can't use it.

Forgetting that struct members default to `private`. Just like classes, struct members are private by default. Writing struct Money { decimal amount; } gives you a struct with one inaccessible field. The fix is to write public explicitly or use auto-properties: struct Money { public decimal Amount { get; } }.

Mixing up `protected internal` and `private protected`. The mnemonic that helps: read each one as "OR" or "AND." protected internal is "protected OR internal" (the union, wider). private protected is "private AND protected" (the intersection, narrower). If you can't remember which is which, consult the visibility matrix.

What's wrong with this code?

The AddProduct method is private, so Main cannot call it even though both live in the same assembly. The fix is to widen the access:

AddProduct is now public so Main can call it. The products list stays private, and a read-only view is exposed via Products. Callers can read the list but can't mutate it directly.

Summary

  • C# has six access modifiers: public, internal, protected, private, protected internal (union), and private protected (intersection). C# 11 added file for source-file-scoped types.
  • The defaults are conservative: top-level types default to internal, class members default to private, struct members default to private, interface members are public.
  • Top-level types can only be public, internal, or file. They cannot be private or protected because there's no enclosing scope.
  • Struct members cannot use the protected variants because structs are implicitly sealed and have no subclasses.
  • protected internal is wider than either keyword alone (same assembly OR subclass anywhere). private protected is narrower than either (same assembly AND subclass).
  • Property accessors can have asymmetric visibility, like public int Count { get; private set; }, to let outside callers read but only the class write.
  • Pick the narrowest modifier that compiles. Wider visibility is a future maintenance cost.
  • Access modifiers are a compile-time concept. They do not affect runtime performance, and reflection can bypass them.