Private Protected vs Protected Internal in C

In C#, private protected and protected internal are compound access modifiers that control the visibility of class members across different assemblies and derived classes. The fundamental difference lies in their evaluation logic: protected internal uses an "OR" relationship, granting access to code in the same assembly or any derived class, while private protected uses an "AND" relationship, restricting access strictly to derived classes within the same assembly.

Protected Internal (Union / OR Logic)

A member declared with the protected internal modifier is accessible to:

  • Any code inside the same assembly (functioning like internal).
  • Any derived class, even if that derived class resides in a completely different assembly (functioning like protected).

This modifier is ideal when you want to allow internal utility classes in your library to access a member while simultaneously allowing external consumers of your library to extend and override that member through inheritance.

Private Protected (Intersection / AND Logic)

Introduced in C# 7.2, a member declared with the private protected modifier is accessible only to:

  • Derived classes that are located within the same assembly.

It prevents external assemblies from accessing the member, even if a class in that external assembly derives from the base class. It also prevents non-derived classes within the same assembly from accessing it. This modifier is best suited for internal framework architectures where base functionality should only be shared with internal subclasses.

Summary Comparison Matrix

Context protected internal private protected
Same Assembly, Derived Class Accessible Accessible
Same Assembly, Non-Derived Class Accessible Not Accessible
Different Assembly, Derived Class Accessible Not Accessible
Different Assembly, Non-Derived Class Not Accessible Not Accessible

Code Example

// Assembly A
public class BaseClass
{
    protected internal int ProtectedInternalField = 1;
    private protected int PrivateProtectedField = 2;
}

public class SameAssemblyDerived : BaseClass
{
    void AccessMembers()
    {
        // Both are accessible here
        int a = ProtectedInternalField;
        int b = PrivateProtectedField;
    }
}

public class SameAssemblyNonDerived
{
    void AccessMembers(BaseClass obj)
    {
        // Accessible because it behaves like 'internal'
        int a = obj.ProtectedInternalField; 
        
        // Compile Error: PrivateProtectedField is only accessible in derived classes
        // int b = obj.PrivateProtectedField; 
    }
}

// Assembly B (references Assembly A)
public class ExternalAssemblyDerived : BaseClass
{
    void AccessMembers()
    {
        // Accessible because it behaves like 'protected'
        int a = ProtectedInternalField; 
        
        // Compile Error: PrivateProtectedField cannot be accessed from another assembly
        // int b = PrivateProtectedField; 
    }
}