What are access modifiers in C# and when would you use each?
Quick Answer
Access modifiers control visibility: public (accessible everywhere), private (only within class), protected (class and derived classes), internal (within assembly), protected internal (protected OR internal), private protected (protected AND internal). Use public for APIs, private for implementation details, protected for inheritance, internal for assembly-level access.
Detailed Answer
Access modifiers control the visibility and accessibility of classes, methods, properties, and other members in C#. They are fundamental to encapsulation and object-oriented design.
Available Access Modifiers:
1. public - Most Permissive
- Accessible from anywhere
- No restrictions on access
2. private - Most Restrictive
- Only accessible within the same class
- Default for class members
3. protected - Family Access
- Accessible within the same class and derived classes
- Not accessible from outside the inheritance hierarchy
4. internal - Assembly Access
- Accessible within the same assembly (project)
- Default for classes and interfaces
5. protected internal - Family or Assembly Access
- Accessible within the same assembly OR derived classes (even in different assemblies)
6. private protected - Family and Assembly Access (C# 7.2+)
- Accessible within the same class, derived classes, AND same assembly
Example:
// Assembly: MyLibrary.dll
namespace MyLibrary
{
// Internal class - only accessible within this assembly
internal class InternalHelper
{
public void DoWork() { }
}
// Public class - accessible from other assemblies
public class BankAccount
{
// Private field - only accessible within this class
private decimal balance;
private string accountNumber;
// Protected field - accessible in derived classes
protected DateTime lastTransactionDate;
// Internal field - accessible within this assembly
internal string internalNotes;
// Protected internal - accessible in derived classes OR same assembly
protected internal string specialNotes;
// Private protected - accessible in derived classes AND same assembly
private protected string confidentialNotes;
// Public constructor
public BankAccount(string accountNumber, decimal initialBalance)
{
this.accountNumber = accountNumber;
this.balance = initialBalance;
lastTransactionDate = DateTime.Now;
}
// Public property - accessible from anywhere
public decimal Balance
{
get { return balance; }
private set // Private setter - only this class can modify
{
if (value < 0)
throw new ArgumentException("Balance cannot be negative");
balance = value;
}
}
// Public method - accessible from anywhere
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
Balance += amount;
lastTransactionDate = DateTime.Now;
LogTransaction("Deposit", amount);
}
// Protected method - accessible in derived classes
protected virtual void LogTransaction(string type, decimal amount)
{
Console.WriteLine($"{type}: {amount:C} on {lastTransactionDate}");
}
// Internal method - accessible within this assembly
internal void InternalAudit()
{
Console.WriteLine($"Internal audit for account {accountNumber}");
}
// Private method - only accessible within this class
private void ValidateAccount()
{
if (string.IsNullOrEmpty(accountNumber))
throw new InvalidOperationException("Invalid account number");
}
}
// Derived class in the same assembly
public class SavingsAccount : BankAccount
{
private decimal interestRate;
public SavingsAccount(string accountNumber, decimal initialBalance, decimal interestRate)
: base(accountNumber, initialBalance)
{
this.interestRate = interestRate;
}
// Can access protected members
public void ApplyInterest()
{
decimal interest = Balance * interestRate;
Balance += interest; // Can access protected setter through property
lastTransactionDate = DateTime.Now; // Can access protected field
LogTransaction("Interest", interest); // Can access protected method
}
// Can access protected internal members
public void UpdateSpecialNotes(string notes)
{
specialNotes = notes; // Accessible
}
// Can access private protected members
public void UpdateConfidentialNotes(string notes)
{
confidentialNotes = notes; // Accessible (same assembly + derived)
}
// Override protected method
protected override void LogTransaction(string type, decimal amount)
{
base.LogTransaction(type, amount);
Console.WriteLine($"Interest Rate: {interestRate:P}");
}
}
}
// Assembly: MyApplication.exe (references MyLibrary.dll)
namespace MyApplication
{
public class Program
{
public static void Main()
{
var account = new BankAccount("123456", 1000);
// Public members - accessible
account.Deposit(500);
Console.WriteLine($"Balance: {account.Balance}");
// Internal members - NOT accessible (different assembly)
// account.InternalAudit(); // Compilation error
// Protected members - NOT accessible (not derived class)
// account.lastTransactionDate = DateTime.Now; // Compilation error
// Private members - NOT accessible
// account.balance = 2000; // Compilation error
var savingsAccount = new SavingsAccount("789012", 2000, 0.05m);
savingsAccount.ApplyInterest();
savingsAccount.UpdateSpecialNotes("VIP Customer");
}
}
// Derived class in different assembly
public class CheckingAccount : BankAccount
{
public CheckingAccount(string accountNumber, decimal initialBalance)
: base(accountNumber, initialBalance)
{
}
// Can access protected members
public void ProcessCheck(decimal amount)
{
Balance -= amount; // Can access protected setter
lastTransactionDate = DateTime.Now; // Can access protected field
}
// Can access protected internal members
public void UpdateSpecialNotes(string notes)
{
specialNotes = notes; // Accessible (derived class)
}
// CANNOT access private protected members (different assembly)
// public void UpdateConfidentialNotes(string notes)
// {
// confidentialNotes = notes; // Compilation error
// }
}
}
Access Modifier Guidelines:
When to use public:
- API surface that external code needs to use
- Properties that represent the object's state
- Methods that provide core functionality
When to use private:
- Implementation details that should be hidden
- Helper methods used only within the class
- Fields that should only be modified through properties
When to use protected:
- Members that derived classes need to access
- Virtual methods that can be overridden
- Fields that derived classes need to modify
When to use internal:
- Classes that are implementation details of your library
- Methods that should only be used within your assembly
- Testing utilities that shouldn't be exposed publicly
When to use protected internal:
- Members that derived classes OR assembly code needs
- Rarely used - consider if you really need this level of access
When to use private protected:
- Members that only derived classes in the same assembly should access
- Very specific use case - rarely needed
Default Access Levels:
- Class members:
private - Classes and interfaces:
internal - Namespaces: Always
public(cannot be modified)
Best Practices:
- Start with the most restrictive access level (
private) - Only increase visibility when necessary
- Use properties instead of public fields
- Prefer
protectedoverprotected internalwhen possible - Document public APIs thoroughly
- Use
internalfor testing utilities