← Open in the full interactive course (progress tracking, search & more)

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.

Access = Visibility

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.

public
Accessible from anywhere
private
Accessible only within the same class/struct
protected
Accessible within the same class and derived classes
internal
Accessible within the same assembly
protected internal
Accessible within same assembly OR derived classes elsewhere
private protected
Accessible within same assembly AND derived classes
file
Accessible only within the same file (C# 11)

Why Does It Exist?

The Problem

Without access control, all code would be visible to everything else. This creates several issues:

The Solution

Access modifiers provide a controlled interface to your types and members:

Big Picture

Here's a visual representation of the accessibility levels:

ACCESS MODIFIERS — VISIBILITY ZONES
Public
Anywhere
Protected
Same class + derived
Internal
Same assembly
Protected Internal
Same assembly OR derived
Private Protected
Same assembly AND derived
Private
Only same class
File (C# 11)
Only same source file
Assembly
Derived classes (outside)
Same class
Same file
Each modifier defines a specific visibility boundary.

How It Works

Let's explore each modifier with examples.

ACCESS MODIFIERS — STEP BY STEP
1. public — Accessible from anywhere
public 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); // works

Public members are part of the type's contract. Use them for your public API.

2. private — Accessible only within the same class/struct
public 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 field

Private is the most restrictive. It's the default for members.

3. protected — Accessible in the same class and derived classes
public 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 outside

Protected is for inheritance hierarchies — allow derived classes to access base members.

4. 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 internal

Internal is used to hide implementation details from other assemblies. The default for top-level types is internal.

5. protected internal — Accessible within same assembly OR from derived classes in other assemblies
public 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.

6. 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.

7. 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 here

The 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 protected

Real-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: private

This design:

Analogy

Access Modifiers = Building Access Levels

Think of a company building:

Each level defines who has the key.

Under the Hood

How does the .NET runtime enforce access modifiers?

ACCESS MODIFIERS — INTERNAL VIEW
1. Compile-time enforcement
2. Runtime enforcement
3. Metadata
4. InternalsVisibleTo

Common Confusion

1. Protected vs Internal

2. Protected Internal vs Private Protected

3. Default Access Modifiers

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?

Guidelines
Avoid

Mental Model

Public = everyone
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


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.
Actually, 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#!


dotnetmadeeasy.com — Learn C# and .NET, the right way.