What are access modifiers in C# and when would you use each?

10 minbeginnerOOPaccess-modifiersencapsulation

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 protected over protected internal when possible
  • Document public APIs thoroughly
  • Use internal for testing utilities

Related Resources