Understanding Access Modifiers in C#

Access modifiers are one of the fundamental building blocks of object-oriented programming in C#. They help you control the visibility and accessibility of your types and members, which is essential for writing secure and maintainable code.

I’ve found that understanding access modifiers thoroughly can significantly improve your code architecture and prevent many common bugs related to inappropriate access to class members.

Core Access Modifiers

C# provides four primary access modifiers that you’ll use regularly:

  • public: Allows access from anywhere in your code
  • private: Restricts access to within the containing class only
  • protected: Allows access within the class and any derived classes
  • internal: Restricts access to within the same assembly

Combination Modifiers

C# also offers two combination modifiers for more specific access control:

Let’s see how these modifiers look in real code:

public class MyClass
{
    // Accessible from anywhere
    public int PublicField;

    // Accessible only within this class
    private string _privateField;

    // Accessible in this class and derived classes
    protected bool ProtectedField;

    // Accessible within this assembly
    internal DateTime InternalField;

    // Accessible in this assembly OR from derived classes anywhere
    protected internal double ProtectedInternalField;

    // Accessible only from derived classes in this assembly
    private protected decimal PrivateProtectedField;
}

Understanding when to use each modifier is a key skill for writing well-structured C# applications, so let’s explore each one in detail.

public - Unrestricted Access

The public modifier is the most permissive access level in C#. When a member is marked as public, it’s accessible from:

  • The containing class itself
  • Other classes in the same project
  • External assemblies that reference your project
  • Derived classes (regardless of where they’re defined)

When to use public

You should use public access for members that:

  • Form part of your API that other developers need to use
  • Represent the essential functionality that your class exposes
  • Need to be accessible from anywhere in your application

Examples in Practice

Here’s a simple example of how public members work:

public class Employee
{
    // Public property -> accessible from anywhere
    public string Name { get; set; }

    // Public method -> can be called from any code
    public void DisplayDetails()
    {
        Console.WriteLine($"Employee: {Name}");
    }
}

You can then access these members from anywhere in your application:

// In another class, even in a different project
Employee employee = new Employee();
employee.Name = "Jane Smith";  // Directly accessing the public property
employee.DisplayDetails();     // Calling the public method

Common Uses

Public members typically include:

  • API endpoints in libraries
  • Service interfaces
  • Public-facing models
  • Controller actions in web applications
  • Property getters for data that needs to be readable anywhere

Best Practice: While public members offer maximum accessibility, they should be used sparingly. Every public member becomes part of your API contract, making future changes more difficult without breaking compatibility.

private - The Most Restrictive Access

The private modifier is the most restrictive access level in C#. It’s the cornerstone of encapsulation, one of the core principles of object-oriented programming.

When a member is marked as private, it can only be accessed from within the containing class itself. This enforces information hiding and helps prevent external code from depending on implementation details.

When to use private

Use private access for:

  • Implementation details that should be hidden from outside classes
  • Internal state that needs protection from external modification
  • Helper methods that are only relevant to the class’s internal workings
  • Fields that should only be manipulated through property accessors

Examples in Practice

Here’s how private members work in real code:

public class BankAccount
{
    // Private field -> only accessible within this class
    private decimal _balance;

    // Private method -> internal implementation detail
    private void ValidateTransaction(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
    }

    // Public method that uses private members
    public void Deposit(decimal amount)
    {
        ValidateTransaction(amount);
        _balance += amount;
    }

    // Property with private setter -> balance can be read but not modified directly
    public decimal Balance
    {
        get { return _balance; }
        private set { _balance = value; }
    }
}

Attempting to access private members from outside the class results in a compilation error:

var account = new BankAccount();
account.Deposit(100);

// These lines would cause compilation errors:
// account._balance = 1000000;        // Can't access private field
// account.ValidateTransaction(50);   // Can't access private method
// account.Balance = 500;             // Can't access private setter

The Power of Encapsulation

Using private members gives you several important benefits:

  1. Change Protection - You can change private implementation details without affecting external code
  2. State Control - You can enforce validation rules when class state is modified
  3. Reduced Complexity - External code only sees what it needs to, simplifying usage

When I’m designing classes, I typically make everything private by default, then selectively expose only what’s necessary through public methods and properties. This approach leads to more maintainable code that’s easier to change over time.

protected - Supporting Inheritance

The protected access modifier is specifically designed for class hierarchies and inheritance scenarios. It strikes a balance between accessibility and encapsulation.

When a member is marked as protected:

  • It’s accessible within the class where it’s declared (like private)
  • It’s also accessible from any class that inherits from that class
  • It’s NOT accessible from other, unrelated classes

When to use protected

Use protected access when:

  • You’re building a class hierarchy where derived classes need access to certain members
  • You want to provide functionality that only makes sense to subclasses
  • You need to allow subclasses to override or extend behavior while hiding it from the rest of the application

Examples in Practice

Here’s a practical example of how protected members support inheritance:

public class Shape
{
    // Protected properties available to derived shapes
    protected double Width { get; set; }
    protected double Height { get; set; }

    // Protected method that derived classes might need
    protected virtual void ValidateDimensions()
    {
        if (Width <= 0 || Height <= 0)
            throw new ArgumentException("Dimensions must be positive");
    }

    // Public constructor
    public Shape(double width, double height)
    {
        Width = width;
        Height = height;
        ValidateDimensions();
    }

    // Public method that derived classes will override
    public virtual double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    private double _radius;

    public Circle(double radius) : base(radius * 2, radius * 2)
    {
        _radius = radius;
    }

    // Override that uses protected members from the base class
    public override double CalculateArea()
    {
        // We can access the protected members from the base class
        ValidateDimensions();
        return Math.PI * _radius * _radius;
    }
}

Code from outside these classes cannot access the protected members:

var circle = new Circle(5);
var area = circle.CalculateArea();  // This works (public method)

// This would cause a compilation error:
// double w = circle.Width;  // Cannot access protected property

Protected vs Private

Choosing between protected and private is about balancing flexibility and encapsulation:

  • Use protected when you’re explicitly designing for inheritance and want to provide “hooks” for subclasses
  • Use private when the member is truly an implementation detail that derived classes shouldn’t depend on

I’ve learned through experience that making too many members protected can lead to fragile base class problems, where changes to the base class unexpectedly break derived classes. Start with private and only elevate to protected when you have a clear inheritance use case.

internal - Assembly-Level Access

The internal access modifier is unique because it works at the assembly level rather than the class level. In C#, an assembly typically corresponds to a compiled DLL or EXE file.

When a member is marked as internal:

  • It’s accessible from any code within the same assembly/project
  • It’s NOT accessible from other assemblies, even if they reference your assembly
  • It allows you to share implementation details within your project without exposing them externally

When to use internal

Use internal access when:

  • You want to share functionality between classes in your project but not expose it publicly
  • You’re creating components that should only be used by other parts of the same library
  • You need to keep implementation details hidden from consumers of your library
  • You’re working on a team project where module boundaries are important

Examples in Practice

Here’s how internal access works in a real project:

// In AssemblyA.dll
namespace MyCompany.DataAccess
{
    // This class can be seen by external assemblies
    public class CustomerRepository
    {
        // But this internal method can only be used within AssemblyA
        internal static void ValidateConnectionString(string connectionString)
        {
            // Validation logic
        }

        public Customer GetCustomer(int id)
        {
            // Public API that internally uses the internal helper
            ValidateConnectionString(_connectionString);
            // Rest of implementation...
        }
    }

    // Internal class - only usable within AssemblyA
    internal class QueryBuilder
    {
        // Implementation details hidden from external assemblies
    }
}

In another assembly (e.g., a web application) that references AssemblyA.dll:

// In WebApp.dll (references AssemblyA.dll)
using MyCompany.DataAccess;

public class CustomerController
{
    private CustomerRepository _repo = new CustomerRepository();

    public ActionResult GetCustomer(int id)
    {
        var customer = _repo.GetCustomer(id);  // Can use public members

        // These would cause compilation errors:
        // CustomerRepository.ValidateConnectionString("...");  // Can't access internal method
        // var builder = new QueryBuilder();  // Can't access internal class

        return View(customer);
    }
}

Internal vs Public

The internal modifier gives you a middle ground between fully public APIs and completely hidden private members:

  • Use public for your stable, documented API that external code should use
  • Use internal for implementation details that need to be shared across your library
  • Use private for member-level implementation details within a specific class

This layered approach to access control helps keep your public API surface small and focused, while still allowing code sharing within your project boundaries.

protected internal - Assembly-Wide Inheritance Access

The protected internal modifier is the first combination modifier in C#, merging the behaviors of protected and internal. It’s important to understand that this is an OR combination, not an AND combination.

When a member is marked as protected internal:

  • It’s accessible from any code within the same assembly (the internal part)
  • It’s accessible from derived classes, even in other assemblies (the protected part)

This creates the second most permissive access level after public.

When to use protected internal

Use protected internal when:

  • You want to allow both inheritance access and assembly-wide access
  • You’re creating framework-level code that needs to be extended across assembly boundaries
  • You need a component to be usable within your library and also inheritable by external code

Visual Access Diagram

Here’s a simple way to visualize who can access a protected internal member:

                       │ Same Assembly │ Different Assembly │
───────────────────────┼───────────────┼────────────────────┤
Same Class             │      Yes      │        Yes         │
───────────────────────┼───────────────┼────────────────────┤
Derived Class          │      Yes      │        Yes         │
───────────────────────┼───────────────┼────────────────────┤
Non-derived Class      │      Yes      │        No          │

Examples in Practice

Here’s a practical example showing how protected internal works:

// In LibraryA.dll
namespace MyFramework.Controls
{
    public class ControlBase
    {
        // This can be accessed by:
        // 1. Any code in LibraryA.dll
        // 2. Any derived class, even in other assemblies
        protected internal void UpdateLayout()
        {
            // Implementation
        }

        // Regular method
        public void Refresh()
        {
            UpdateLayout();
        }
    }
}

// Also in LibraryA.dll
namespace MyFramework.Utilities
{
    public class ControlHelper
    {
        public static void OptimizeControl(ControlBase control)
        {
            // This works because we're in the same assembly
            // even though this class doesn't inherit from ControlBase
            control.UpdateLayout();
        }
    }
}

Then in an external application:

// In Application.exe (references LibraryA.dll)
using MyFramework.Controls;

// Custom control in the application
public class MySpecializedControl : ControlBase
{
    public void CustomLayout()
    {
        // This works because we're inheriting from ControlBase
        // even though we're in a different assembly
        UpdateLayout();
    }
}

public class MainForm
{
    public void SetupControls()
    {
        var control = new ControlBase();

        // This would NOT work - we're in a different assembly
        // and MainForm doesn't inherit from ControlBase
        // control.UpdateLayout();  // Compilation error
    }
}

Real-World Usage

In my experience, protected internal is useful in framework development where you want to provide extension points through inheritance while also allowing internal framework components to access those members. It’s less commonly used in regular application development.

private protected - Restricted Inheritance Access

The private protected modifier is the most restrictive combination modifier, added in C# 7.2. Unlike protected internal (which is an OR combination), private protected is an AND combination of restrictions.

When a member is marked as private protected:

  • It’s accessible only to derived classes within the same assembly
  • It’s NOT accessible from other assemblies, even for derived classes
  • It’s NOT accessible from non-derived classes, even within the same assembly

When to use private protected

Use private protected when:

  • You want to restrict inheritance-based access to the current assembly only
  • You want to provide functionality to derived classes but ensure those derived classes are within your control
  • You need to share implementation details with subclasses without exposing them broadly

Visual Access Diagram

Here’s a clear way to visualize who can access a private protected member:

                       │ Same Assembly │ Different Assembly │
───────────────────────┼───────────────┼────────────────────┤
Same Class             │      Yes      │        Yes         │
───────────────────────┼───────────────┼────────────────────┤
Derived Class          │      Yes      │        No          │
───────────────────────┼───────────────┼────────────────────┤
Non-derived Class      │      No       │        No          │

Examples in Practice

Here’s how private protected works in real code:

// In CoreLibrary.dll
namespace MyCompany.Core
{
    public class DatabaseComponent
    {
        // Only accessible in this class and derived classes in this assembly
        private protected string ConnectionString { get; set; }

        // Only visible to derived classes in this assembly
        private protected void ExecuteWithRetry(Action action)
        {
            // Retry logic implementation
            for (int i = 0; i < 3; i++)
            {
                try
                {
                    action();
                    return;
                }
                catch (Exception ex)
                {
                    if (i == 2) throw;
                    // Log and retry
                }
            }
        }
    }

    // In the same assembly - has access
    public class SqlDatabaseComponent : DatabaseComponent
    {
        public void RunQuery(string query)
        {
            // Can access private protected members
            ExecuteWithRetry(() => {
                // Use ConnectionString to run query
            });
        }
    }

    // In the same assembly but not derived - NO access
    public class Logger
    {
        public void LogConnection(DatabaseComponent component)
        {
            // This would NOT compile:
            // string connection = component.ConnectionString;
        }
    }
}

Then in an external assembly:

// In ExternalApp.dll (references CoreLibrary.dll)
using MyCompany.Core;

// External derived class - NO access to private protected members
public class CustomDatabaseComponent : DatabaseComponent
{
    public void CustomQuery()
    {
        // These would NOT compile:
        // string conn = ConnectionString;
        // ExecuteWithRetry(() => { });
    }
}

When to Choose private protected over protected

The private protected modifier gives you finer control than regular protected:

  • Use protected when you want to support inheritance by anyone
  • Use private protected when you only want to support inheritance within your assembly

This is particularly valuable when developing libraries where you want to provide extension points for your own code, but not expose those implementation details to external consumers.

Choosing the Right Access Modifier

The access modifier you choose has a significant impact on code maintainability, security, and flexibility. Here’s a quick decision guide:

  1. Start with the most restrictive access possible (private)
  2. Elevate access only when there’s a specific need:
    • Need to expose functionality to everyone? → public
    • Need to share within your assembly only? → internal
    • Need to support inheritance? → protected or private protected
    • Need both assembly and inheritance access? → protected internal

Comparison Table

Same ClassDerived ClassSame AssemblyDifferent Assembly
public
protected❌*❌*
internal❌*
private
protected internal❌**
private protected✅***❌*

*Unless the class itself is derived
**Unless it’s a derived class
***Only within the same assembly

Best Practices

From my years of C# development experience, I’ve found these access modifier practices to be helpful:

  1. Default to Private - Start everything as private and only increase visibility when needed
  2. Minimize Public - Keep your public API surface as small as possible
  3. Use Internal for Components - internal is great for implementation details shared across your library
  4. Think Long-term - Remember that every public and protected member becomes part of your API contract
  5. Consider Interfaces - Instead of making implementation classes public, consider exposing interfaces instead

By thoughtfully applying access modifiers, you’ll create code that’s more maintainable, secure, and easier to understand.