Introduction
Access specifiers in C# are used to define the accessibility or visibility of types and their members.
In simple terms, an access specifier determines who can access a class, method, property, field, constructor, or other member and from where it can be accessed.
For example, suppose a class contains several methods. Some methods may need to be available to every class in an application, while others may need to be accessible only within the same class or within derived classes.
C# provides access modifiers to control this visibility and help developers implement encapsulation.
Consider the following example:
public class Employee
{
private string employeeName;
public void SetEmployeeName(string name)
{
employeeName = name;
}
public string GetEmployeeName()
{
return employeeName;
}
}
Here, employeeName is declared as private, so it cannot be accessed directly from outside the Employee class. The public methods provide controlled access to the value.
This is one of the fundamental concepts of object-oriented programming in C#.
Different Types of Access Specifiers in C#
C# provides six commonly used access modifiers:
private
public
protected
internal
protected internal
private protected
The private protected access modifier was introduced in C# 7.2.
The accessibility provided by these modifiers depends on whether the accessing code is located in the same class, the same assembly, or another assembly, and whether inheritance is involved.
1. Private Access Modifier
The private access modifier restricts access to the containing type.
A private member can be accessed from within the class where it is declared, but it cannot normally be accessed directly from derived classes or unrelated classes.
Example
public class Employee
{
private string employeeName = "John";
public void DisplayName()
{
Console.WriteLine(employeeName);
}
}
The employeeName field can be accessed inside the Employee class:
Employee employee = new Employee();
employee.DisplayName();
But the following code is not allowed:
Employee employee = new Employee();
// Compile-time error
// employee.employeeName = "David";
Accessibility of a Private Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | No |
Non-derived class in same assembly | No |
Derived class in another assembly | No |
Non-derived class in another assembly | No |
private is useful when an implementation detail should be completely hidden from other types.
2. Public Access Modifier
The public access modifier provides the widest accessibility.
A public member can be accessed from code that can access the containing type, including code in other assemblies.
Example
public class Employee
{
public string EmployeeName = "John";
public void DisplayName()
{
Console.WriteLine(EmployeeName);
}
}
The member can be accessed from another class:
Employee employee = new Employee();
Console.WriteLine(employee.EmployeeName);
employee.DisplayName();
It can also be accessed from a derived class:
public class Manager : Employee
{
public void DisplayManagerName()
{
Console.WriteLine(EmployeeName);
}
}
Accessibility of a Public Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | Yes |
Non-derived class in same assembly | Yes |
Derived class in another assembly | Yes |
Non-derived class in another assembly | Yes |
Public members should generally represent functionality that is intentionally exposed as part of a type's API.
3. Protected Access Modifier
The protected access modifier allows access within the containing type and from derived types.
Unlike public, a protected member cannot normally be accessed through an object from an unrelated class.
Example
public class Employee
{
protected string employeeName = "John";
}
public class Manager : Employee
{
public void DisplayName()
{
Console.WriteLine(employeeName);
}
}
Manager can access employeeName because Manager derives from Employee.
However, an unrelated class cannot access it directly:
public class Test
{
public void Display()
{
Employee employee = new Employee();
// Compile-time error
// Console.WriteLine(employee.employeeName);
}
}
Accessibility of a Protected Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | Yes |
Non-derived class in same assembly | No |
Derived class in another assembly | Yes |
Non-derived class in another assembly | No |
protected is commonly used when a base class needs to expose implementation details to derived classes without making those details publicly accessible.
4. Internal Access Modifier
The internal access modifier restricts access to the current assembly.
An assembly is typically the compiled output of a .NET project, such as a .dll or .exe.
An internal member can be accessed by other types within the same assembly but not directly from another assembly.
Example
internal class Employee
{
internal string EmployeeName = "John";
}
Another class in the same project can access it:
public class Department
{
public void DisplayEmployee()
{
Employee employee = new Employee();
Console.WriteLine(employee.EmployeeName);
}
}
However, code in another assembly cannot directly access the internal type or member unless mechanisms such as InternalsVisibleTo are used.
Accessibility of an Internal Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | Yes |
Non-derived class in same assembly | Yes |
Derived class in another assembly | No |
Non-derived class in another assembly | No |
internal is useful when functionality needs to be shared among types within a project but should not form part of the project's public API.
5. Protected Internal Access Modifier
protected internal combines two accessibility conditions.
A member declared as protected internal can be accessed:
From anywhere within the same assembly, or
From a derived class, including a derived class in another assembly.
The important point is that this is effectively an OR relationship between protected and internal.
Example
public class Employee
{
protected internal string EmployeeName = "John";
}
A class in the same assembly can access the member:
public class Department
{
public void DisplayEmployee()
{
Employee employee = new Employee();
Console.WriteLine(employee.EmployeeName);
}
}
A derived class in another assembly can also access the member.
Accessibility of a Protected Internal Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | Yes |
Non-derived class in same assembly | Yes |
Derived class in another assembly | Yes |
Non-derived class in another assembly | No |
Because protected internal provides relatively broad access, it should be used when both same-assembly access and derived-type access are intentionally required.
6. Private Protected Access Modifier
The private protected access modifier provides more restricted access than protected internal.
A private protected member can be accessed only:
It was introduced in C# 7.2.
Example
public class Employee
{
private protected string employeeName = "John";
}
public class Manager : Employee
{
public void DisplayName()
{
Console.WriteLine(employeeName);
}
}
The Manager class can access employeeName because it derives from Employee and is in the same assembly.
A derived class in another assembly cannot access the member.
Accessibility of a Private Protected Member
Accessing Code | Accessible? |
|---|
Containing class | Yes |
Derived class in same assembly | Yes |
Non-derived class in same assembly | No |
Derived class in another assembly | No |
Non-derived class in another assembly | No |
This modifier is useful when a base class wants to expose a member only to derived types that are part of the same assembly.
Access Modifier Comparison
The following table provides a quick comparison:
Access Modifier | Same Class | Same Assembly | Derived Type in Other Assembly | Non-Derived Type in Other Assembly |
|---|
private
| Yes | No | No | No |
public
| Yes | Yes | Yes | Yes |
protected
| Yes | No for unrelated types | Yes for derived types | No |
internal
| Yes | Yes | No | No |
protected internal
| Yes | Yes | Yes for derived types | No |
private protected
| Yes | Yes for derived types | No | No |
The key distinction to remember is:
protected internal = protected OR internal
private protected = protected AND internal
This makes the difference between the two modifiers easier to understand.
Access Modifiers for Types
Access modifiers can also be applied to types such as classes, interfaces, structs, delegates, and enums, subject to the accessibility rules of each type.
For example:
public class Employee
{
}
The Employee class is publicly accessible.
An internal class can be declared as:
internal class Department
{
}
The Department type is accessible only within the same assembly.
For top-level types, private and protected are not valid access modifiers. They are primarily used for members and nested types.
Why Access Modifiers Are Important
Access modifiers are an important part of encapsulation.
They allow developers to decide which parts of a class should be exposed and which implementation details should remain hidden.
For example:
public class BankAccount
{
private decimal balance;
public void Deposit(decimal amount)
{
if (amount > 0)
{
balance += amount;
}
}
public decimal GetBalance()
{
return balance;
}
}
Here, balance is private. External code cannot directly change it:
BankAccount account = new BankAccount();
// Not allowed
// account.balance = -5000;
Instead, the class controls how the balance changes through the public Deposit method.
This helps protect the internal state of the object and keeps business rules inside the class.
Common Mistakes
Making Everything Public
A common beginner mistake is declaring every field and method as public.
For example:
public class Employee
{
public string name;
public decimal salary;
}
This exposes the internal state of the class unnecessarily.
A better approach is to expose only what other parts of the application actually need.
Confusing Protected and Internal
protected is primarily related to inheritance, while internal is related to the assembly boundary.
For example:
protected
-> containing type + derived types
internal
-> types within the same assembly
Understanding these two boundaries makes the other access modifiers much easier to understand.
Confusing Protected Internal and Private Protected
These two modifiers look similar but provide different levels of access.
protected internal
= protected OR internal
private protected
= protected AND internal
Therefore, protected internal is broader, while private protected is more restrictive.
Best Practices
When choosing an access modifier, consider the smallest scope required by the member.
Some general guidelines are:
Use private for implementation details that should remain inside the type.
Use public only when functionality needs to be exposed to consumers.
Use protected when derived classes need access to a member.
Use internal for functionality that should remain within the current assembly.
Use protected internal when both same-assembly access and derived-type access are required.
Use private protected when access should be limited to derived types within the same assembly.
Prefer encapsulation instead of exposing internal fields directly.
Conclusion
Access specifiers in C# provide a mechanism for controlling the accessibility and visibility of types and their members.
The six commonly used access modifiers are private, public, protected, internal, protected internal, and private protected.
Understanding the difference between these modifiers is important for designing classes with proper encapsulation and well-defined APIs.
For beginners, the easiest way to remember them is to focus on two boundaries: inheritance and assembly. Once these boundaries are clear, choosing the appropriate access modifier becomes much easier.
Join the conversation! Your thoughts help the community grow.