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.
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.
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.
Without access control, all code would be visible to everything else. This creates several issues:
Access modifiers provide a controlled interface to your types and members:
Here's a visual representation of the accessibility levels:
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.
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 protectedIn 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:
Logger and ILogWriter are the public contract.DefaultFormat and ConsoleWriter are implementation details.OnLog allows derived classes to add behaviour.FormatMessage is an internal detail.SetWriter allows derived classes and test assemblies to inject a writer.Think of a company building:
Each level defines who has the key.
How does the .NET runtime enforce access modifiers?
MethodAttributes, FieldAttributes, etc.internal members visible to specific friend assemblies using the InternalsVisibleTo attribute.private by default.internal by default.private by default.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
}If you want derived classes in other assemblies to access a member, you need protected or protected internal, not internal.
Many developers confuse the two. Remember: private protected is stricter (requires both same assembly and derived class).
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.
private is the default for members; internal is the default for top-level types.
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?
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#?
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?
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?
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 { /* ... */ } 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#!
dotnetmadeeasy.com — Learn C# and .NET, the right way.