C# Access Modifiers: public, private, protected and internal
Access modifiers control who can see and use your types and members.
Imagine you're building a house. You have a front door (public) that anyone can use, a back door that only family can use (internal), and a bedroom that only you can enter (private). In C#, access modifiers are the locks and keys that control who can access your classes, methods, and fields.
They're essential for encapsulation — hiding internal details and exposing only what's necessary. They help you design clean APIs, protect your data, and make your code easier to maintain and evolve.
In this lesson, you'll learn about each access modifier: public, private, protected, internal, protected internal, private protected, and the file modifier introduced in C# 11.
What Is It?
The Simple Explanation
Access modifiers are keywords that specify the visibility of types and their members. They determine which parts of your code can access a particular class, method, field, or property.
Think of access modifiers as the visibility settings for your code. They decide who gets to see and use what.
The default accessibility for members is private; for types, it's internal.
The Technical Definition
Access modifiers are keywords applied to type declarations (classes, structs, interfaces, etc.) and their members (fields, methods, properties, events, etc.) to control their accessibility from other code. The .NET runtime enforces these rules at compile time and runtime.
Why Does It Exist?
The Problem
Without access control, all code would be visible to everything else. This creates several issues:
- No encapsulation — anyone can change internal state, breaking invariants.
- Fragile APIs — changing internal details breaks external code.
- Hard to reason about — it's unclear what is intended for public use vs internal.
- Security risks — sensitive data or methods could be misused.
The Solution
Access modifiers provide a controlled interface to your types and members:
- Encapsulation — hide implementation details behind a public API.
- Abstraction — expose only what's needed.
- Maintainability — you can change internal code without breaking users.
- Security — restrict access to sensitive members.
- Design clarity — clearly separates public contract from private implementation.
Big Picture
Here's a visual representation of the accessibility levels:
How It Works
Let's explore each modifier with examples.
public — Accessible from anywherepublic class Calculator
{
public int Add(int a, int b) => a + b; // accessible everywhere
}
// In another assembly:
var calc = new Calculator();
int sum = calc.Add(3, 5); // worksPublic members are part of the type's contract. Use them for your public API.
private — Accessible only within the same class/structpublic class Person
{
private int _age; // only accessible inside Person
public void SetAge(int age)
{
if (age >= 0) _age = age; // allowed
}
}
var p = new Person();
p._age = 20; // Error! Cannot access private fieldPrivate is the most restrictive. It's the default for members.
protected — Accessible in the same class and derived classespublic class Animal
{
protected string Species { get; set; } // accessible in derived classes
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"A {Species} barks!"); // allowed
}
}
var animal = new Animal();
animal.Species = "Canine"; // Error! protected member not accessible from outsideProtected is for inheritance hierarchies — allow derived classes to access base members.
internal — Accessible within the same assembly// In AssemblyA.dll
internal class Helper // only visible inside AssemblyA
{
public void DoWork() { }
}
// In AssemblyB.dll (references AssemblyA)
var h = new Helper(); // Error! Helper is internalInternal is used to hide implementation details from other assemblies. The default for top-level types is internal.
protected internal — Accessible within same assembly OR from derived classes in other assembliespublic class Base
{
protected internal int Value { get; set; }
}
// Same assembly:
var b = new Base();
b.Value = 10; // allowed (internal part)
// Derived class in another assembly:
public class Derived : Base
{
public void Update() => Value = 20; // allowed (protected part)
}It's a union: accessible to any code in the same assembly or to derived classes regardless of assembly.
private protected — Accessible within same assembly AND from derived classes (intersection)public class Base
{
private protected int Value { get; set; }
}
// Same assembly, derived class:
public class DerivedInSameAssembly : Base
{
public void Update() => Value = 10; // allowed (same assembly + derived)
}
// Different assembly, derived class:
public class DerivedOther : Base
{
public void Update() => Value = 20; // Error! Not accessible outside assembly
}It's an intersection: accessible only to derived classes and only within the same assembly.
file — Accessible only within the same source file (C# 11)// File1.cs
file class FileLocalHelper // only visible in File1.cs
{
public void DoWork() { }
}
// File2.cs
var h = new FileLocalHelper(); // Error! FileLocalHelper not visible hereThe file modifier is useful for types that are only used within a single file, helping to reduce name collisions.
Simple Example
Let's see all modifiers in a single class:
public class LibraryItem
{
// Public — accessible everywhere
public string Title { get; set; }
// Private — only inside this class
private DateTime _createdAt = DateTime.UtcNow;
// Protected — accessible in derived classes
protected string InternalId { get; set; }
// Internal — accessible within the same assembly
internal string Location { get; set; }
// Protected internal — same assembly OR derived classes
protected internal string Notes { get; set; }
// Private protected — same assembly AND derived classes
private protected string ArchiveKey { get; set; }
public void Display()
{
Console.WriteLine($"Title: {Title}");
Console.WriteLine($"Created: {_createdAt}"); // private accessible here
Console.WriteLine($"InternalId: {InternalId}"); // protected accessible here
Console.WriteLine($"Location: {Location}"); // internal accessible here
Console.WriteLine($"Notes: {Notes}"); // protected internal accessible here
Console.WriteLine($"ArchiveKey: {ArchiveKey}"); // private protected accessible here
}
}
public class Book : LibraryItem
{
public void ShowDetails()
{
Console.WriteLine(Title); // public
// Console.WriteLine(_createdAt); // Error: private
Console.WriteLine(InternalId); // protected (allowed)
Console.WriteLine(Location); // internal (same assembly)
Console.WriteLine(Notes); // protected internal (allowed)
Console.WriteLine(ArchiveKey); // private protected (allowed if same assembly)
}
}
// In the same assembly:
var item = new LibraryItem();
item.Title = "C# Programming"; // public
// item._createdAt = ...; // Error: private
// item.InternalId = ...; // Error: protected
item.Location = "Shelf A"; // internal (same assembly)
item.Notes = "Handle with care"; // protected internal (same assembly)
// item.ArchiveKey = ...; // Error: private protectedReal-World Example
In a library or framework, you typically expose a public API, hide implementation details with private and internal, and allow extensibility with protected.
// In a logging library (assembly: LoggingLibrary.dll)
// Public API — consumers use this
public class Logger
{
private ILogWriter _writer; // private dependency
// Internal helper — only used within the assembly
internal static string DefaultFormat = "[{Timestamp}] {Level}: {Message}";
// Protected — for extensibility by derived classes
protected virtual void OnLog(LogLevel level, string message)
{
// Hook for derived classes
}
// Public method — the main API
public void Log(LogLevel level, string message)
{
var formatted = FormatMessage(level, message);
_writer.Write(formatted);
OnLog(level, message);
}
// Private helper — internal implementation detail
private string FormatMessage(LogLevel level, string message)
{
return DefaultFormat
.Replace("{Timestamp}", DateTime.UtcNow.ToString("o"))
.Replace("{Level}", level.ToString())
.Replace("{Message}", message);
}
// Protected internal — accessible to derived classes and same assembly tests
protected internal void SetWriter(ILogWriter writer)
{
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
}
}
// Internal class — not visible outside the assembly
internal class ConsoleWriter : ILogWriter
{
public void Write(string content) => Console.WriteLine(content);
}
// Public interface — consumers can provide custom writers
public interface ILogWriter
{
void Write(string content);
}
// Usage in another assembly:
var logger = new Logger();
logger.Log(LogLevel.Info, "Application started.");
// logger.DefaultFormat = ...; // Error: internal
// logger.FormatMessage(...); // Error: privateThis design:
- Public —
LoggerandILogWriterare the public contract. - Internal —
DefaultFormatandConsoleWriterare implementation details. - Protected —
OnLogallows derived classes to add behaviour. - Private —
FormatMessageis an internal detail. - Protected internal —
SetWriterallows derived classes and test assemblies to inject a writer.
Analogy
Think of a company building:
- Public — the main entrance, open to anyone.
- Private — the CEO's office, only the CEO can enter.
- Protected — the executive lounge, accessible to executives and their direct reports (inheritance).
- Internal — the employee-only areas, only people who work in this building (assembly) can enter.
- Protected internal — the conference room, accessible to employees of this company OR to external partners who are authorized (derived classes).
- Private protected — the company's secret lab, only accessible to employees who are also in a specific department (same assembly AND derived).
- File — a single room that only people in that specific room (file) can access.
Each level defines who has the key.
Under the Hood
How does the .NET runtime enforce access modifiers?
- The C# compiler checks accessibility during compilation and emits errors if you try to access an inaccessible member.
- The compiler also emits metadata (attributes and flags) into the assembly that describe the accessibility of each type and member.
- The runtime (CLR) also enforces accessibility when code is JIT-compiled and executed.
- If you attempt to access a private field via reflection, it's allowed only if the caller has sufficient permissions (e.g., full trust).
- Security transparency and Code Access Security (CAS) can further restrict access.
- Access modifiers are encoded in the assembly metadata as
MethodAttributes,FieldAttributes, etc. - Tools like IntelliSense and documentation generators read this metadata to show which members are accessible.
- You can make
internalmembers visible to specific friend assemblies using theInternalsVisibleToattribute. - This is commonly used for unit testing to allow test assemblies to access internal members.
Common Confusion
1. Protected vs Internal
- Protected — accessible in derived classes, regardless of assembly.
- Internal — accessible in the same assembly, regardless of inheritance.
- They serve different purposes: protected for inheritance, internal for assembly-level sharing.
2. Protected Internal vs Private Protected
- Protected internal — accessible if you are in the same assembly or in a derived class (union).
- Private protected — accessible only if you are in the same assembly and in a derived class (intersection).
- Remember: OR vs AND.
3. Default Access Modifiers
- Members of a class (fields, methods, properties) are
privateby default. - Top-level types (classes, structs, interfaces) are
internalby default. - Nested types (inside a class) are
privateby default.
Common Mistakes
Mistake 1 — Making everything public
Wrong:
public class BankAccount
{
public decimal Balance; // Anyone can change it directly
}Correct:
public class BankAccount
{
private decimal _balance;
public decimal Balance => _balance; // read-only view
}Mistake 2 — Using internal when you mean protected internal
If you want derived classes in other assemblies to access a member, you need protected or protected internal, not internal.
Mistake 3 — Forgetting that private protected is more restrictive than protected internal
Many developers confuse the two. Remember: private protected is stricter (requires both same assembly and derived class).
Mistake 4 — Not using file for file-local types
In C# 11, you can use file to prevent types from being used outside their file. This avoids name collisions and clarifies that a type is only used within that file.
When Should I Use It?
- Use public for your API surface.
- Use private for implementation details.
- Use protected for members that derived classes might need to override or use.
- Use internal for members that should be shared within the assembly but not exposed outside.
- Use protected internal for members that are part of the inheritance contract and also need to be used within the assembly.
- Use private protected for members that should only be accessible to derived classes within the same assembly.
- Use file for types that are only used in a single source file.
- Making everything public — it breaks encapsulation.
- Using protected when the member is not meant to be overridden.
- Using internal when you need to expose the member to derived classes in other assemblies.
- Overcomplicating with protected internal or private protected when a simpler modifier suffices.
Mental Model
Private = only me (the class)
Protected = me + my children (derived classes)
Internal = me + my colleagues (same assembly)
Protected internal = me + my children OR my colleagues
Private protected = me + my children AND my colleagues
File = only people in this room (same file)
Remember:
· Start with the most restrictive modifier that still allows the required access.
· Expose as little as possible to keep your API clean and maintainable.
·
private is the default for members; internal is the default for top-level types.
Key Takeaway
- Access modifiers control visibility — they are the foundation of encapsulation in C#.
- Use the principle of least privilege — start with the most restrictive and widen only as needed.
- Public is for your API; private and internal are for implementation details.
- Protected and its combinations support inheritance hierarchies.
- File (C# 11) helps keep types scoped to a single file.
- Good use of access modifiers leads to cleaner, more maintainable, and secure code.
Check Your Understanding
You've seen how access modifiers control visibility. Let's test your knowledge.
1. Which access modifier makes a member accessible only within the same class?
Show answer
Correct: B — private
Why B is correct: private members are accessible only within the same class or struct. This is the most restrictive level and the default for members.
Why A is incorrect: public is accessible everywhere.
Why C is incorrect: protected is accessible in derived classes as well.
Why D is incorrect: internal is accessible within the same assembly.
Reinforcement: private is the most restrictive; use it for implementation details.
2. What is the default accessibility for a top-level class (not nested) in C#?
Show answer
Correct: C — internal
Why C is correct: In C#, top-level types (classes, structs, interfaces, enums) have internal accessibility by default. This means they are visible only within the same assembly unless explicitly marked public.
Why A is incorrect: public is not the default; you must explicitly add it.
Why B is incorrect: private is not allowed for top-level types (only nested types can be private).
Why D is incorrect: protected is not valid for top-level types.
Reinforcement: Top-level types are internal by default; members are private by default.
3. Which modifier allows access from both within the same assembly and from derived classes in other assemblies?
Show answer
Correct: A — protected internal
Why A is correct: protected internal is a union: it allows access from any code in the same assembly or from derived classes (even in other assemblies).
Why B is incorrect: private protected is an intersection: requires both same assembly and derived class.
Why C is incorrect: internal only allows access within the same assembly, not from derived classes elsewhere.
Why D is incorrect: protected allows access from derived classes anywhere, but not from arbitrary code in the same assembly.
Reinforcement: protected internal is the most permissive combination after public.
4. What is the purpose of the file access modifier introduced in C# 11?
Show answer
Correct: B
Why B is correct: The file modifier (C# 11) restricts the visibility of a type to the source file in which it is declared. This is useful for types that are only used internally within a single file, helping to avoid name collisions and clarify intent.
Why A is incorrect: file is about file scope, not namespace.
Why C is incorrect: That's the purpose of internal.
Why D is incorrect: That's the purpose of protected.
Reinforcement: file is the most restrictive scope for types, even more restrictive than private (which applies to nested types).
5. Consider the following code. Which members are accessible in the Derived class from the same assembly?
public class Base
{
private int a;
protected int b;
internal int c;
protected internal int d;
private protected int e;
public int f;
}
public class Derived : Base { /* ... */ }Show answer
Correct: C — b, c, d, f
Why C is correct: In the same assembly, Derived (which inherits Base) can access:
b(protected) — accessible via inheritance.c(internal) — accessible because same assembly.d(protected internal) — accessible via inheritance or assembly.f(public) — accessible everywhere.e(private protected) — requires both same assembly and derived class, which is true here. Wait, that's actually accessible! Let's re-evaluate.
private protected requires both same assembly and derived class. Since Derived is in the same assembly and inherits, e is accessible. So the correct set should be b, c, d, e, f. Let's check again.
Wait, but option A is b, c, d, e, f. Option A is correct if e is accessible. Let's verify: private protected means accessible only from derived classes within the same assembly. Since Derived is in the same assembly and derived from Base, e is accessible. So the correct answer is A.
But let's double-check: The question says "Which members are accessible in the Derived class from the same assembly?" So all except a (private) are accessible. So answer should be A.
Why B is incorrect: It misses c (internal) which should be accessible.
Why C is incorrect: It misses e (private protected) which is accessible.
Why D is incorrect: It misses c and e.
Reinforcement: private protected is accessible when the conditions of both private (same assembly) and protected (derived class) are met.
Correction: The correct answer is A.
You now have a solid understanding of access modifiers — the keys to encapsulation and API design in C#!