Back to Blog
C#

C# Access Modifiers in Inheritance

c# access modifiers in inheritance: Understand how access modifiers behave in C# inheritance: which members are visible to derived classes, how overriding interacts wi...

C# inheritanceaccess modifiersprotected membersvirtual and overrideOOP in C#private protected
C# class inheritance diagram showing access modifiers controlling member visibility from derived classes

When you derive a class in C#, the access modifier on a base member determines whether the derived class can see it, call it, or override it. The rules are not always intuitive, especially with protected internal and private protected. This article explains how c# access modifiers in inheritance work, with concrete examples and the reasoning behind each behavior.

The Accessibility Hierarchy in Derived Classes

C# defines six access modifiers: public, protected, internal, protected internal, private protected, and private. In an inheritance context, the key question is: which members of the base class are accessible from the derived class?

  • public members are accessible everywhere.
  • protected members are accessible from the derived class and from other types in the same assembly that derive from the base class.
  • internal members are accessible from any type in the same assembly, but not from derived classes in other assemblies.
  • protected internal is a union: accessible from the derived class (even across assemblies) or from any type in the same assembly.
  • private protected is an intersection: accessible only from derived classes that are also in the same assembly.
  • private members are accessible only within the declaring class, not from derived classes.

The derived class always sees public and protected members, regardless of assembly. internal members are visible only if the derived class is in the same assembly. protected internal is visible if either condition holds. private protected requires both: the derived class and same assembly. private members are never directly accessible from a derived class.

Minimal Example: Private and Protected Members

Consider a base class with a private field and a protected method:

public class Base { private int _secret = 42; protected int GetSecret() => _secret; } public class Derived : Base { public void ShowSecret() { // Console.WriteLine(_secret); // Compile error: 'Base._secret' is inaccessible Console.WriteLine(GetSecret()); // OK, protected method } }

The derived class cannot access _secret directly because it is private to Base. It can call GetSecret() because that method is protected. This is the fundamental rule: private members are not inherited in terms of accessibility, but protected members are.

Overriding and Access Modifiers

When you override a virtual or abstract member, the access modifier must match the original declaration. You cannot change the accessibility of an override. For example:

public class Base { protected virtual void Display() { } } public class Derived : Base { // protected override void Display() { } // Correct // public override void Display() { } // Compile error: 'Display()' cannot change access modifiers }

The compiler enforces this because changing the access level would break the contract established by the base class. A caller who holds a reference to the base type expects the member to be accessible at the original level. If a derived class made it more restrictive, that would violate the Liskov substitution principle; if it made it more permissive, it would expose members that the base class intended to keep hidden.

The Difference Between protected internal and private protected

These two modifiers are often confused because they sound similar. The distinction matters when you have derived classes in different assemblies.

  • protected internal means the member is accessible from the same assembly or from any derived class, even if that derived class is in a different assembly.
  • private protected means the member is accessible only from derived classes that are also in the same assembly. It is not accessible from unrelated classes in the same assembly, nor from derived classes in other assemblies.

Example:

// Assembly A public class Base { protected internal int Value1; private protected int Value2; } // Assembly B (references A) public class Derived : Base { public void Access() { Value1 = 1; // OK, because protected internal includes protected // Value2 = 2; // Error: private protected requires same assembly } }

In practice, private protected is useful when you want to expose a member only to specific derived classes that you control within the same assembly, while preventing external derived classes from seeing it. protected internal is broader and is often used in library design where you want internal code to use a member but also allow external derived classes to access it.

Virtual, Abstract, and Access Modifiers

When you declare a member as virtual or abstract, the access modifier still controls visibility. However, there is an additional constraint: an abstract member cannot be private because it must be overridden in a derived class, and a private member is not accessible there. Similarly, a virtual member can be private? Actually, C# does not allow private virtual because a private member cannot be overridden—it is not visible to derived classes. The compiler rejects private virtual.

Common valid combinations are public virtual, protected virtual, internal virtual, protected internal virtual, and private protected virtual. The same applies to abstract members, except they cannot be private or private protected? Actually, private protected abstract is allowed? Let's check: private protected is accessible from derived classes in the same assembly, so an abstract member with that modifier can be overridden by derived classes in the same assembly. That is valid. But private abstract is not allowed because a private member cannot be overridden.

Here is an example of a protected abstract member:

public abstract class Shape { protected abstract double Area(); } public class Circle : Shape { private double _radius; protected override double Area() => Math.PI * _radius * _radius; }

The Area method is protected, so only derived classes can call it. This is a common pattern when you want to force derived classes to implement a calculation but keep it hidden from external callers.

Maintainability and API Design Considerations

Choosing the right access modifier in an inheritance hierarchy is a design decision that affects how your API evolves. protected members become part of the contract for derived classes, so changing them later can break subclasses. private protected gives you more flexibility because it limits the impact to the same assembly, but it also restricts external inheritance.

When you design a base class, ask yourself: should external derived classes be able to call this method? If yes, use protected. If only your own assembly's derived classes should see it, use private protected. If you need to share the member with unrelated classes in the same assembly, use internal or protected internal depending on whether derived classes in other assemblies should also see it.

A common mistake is marking a member public when it is only meant for derived classes. This exposes it to all consumers and makes the public API larger than necessary. Using protected or protected internal keeps the member visible to the intended audience without leaking it globally.

Another pitfall is trying to reduce the accessibility of an overridden member. The compiler prevents this, but sometimes developers attempt to hide a base method with new instead of override. Using new with a more restrictive access modifier is allowed, but it changes the semantics: the base method is still there, and a call through a base reference will invoke the base version. This can lead to confusing behavior.

Compatibility and Versioning

Access modifiers also affect binary compatibility. Changing a protected member to private in a base class will break derived classes that use it. Changing it to protected internal is usually safe because it expands accessibility, but it might expose the member to more code than intended. The safest approach is to start with the most restrictive modifier that satisfies your current needs, and expand later if necessary. However, expanding from private to protected is a breaking change for derived classes that could not access it before? Actually, it is not breaking—it adds accessibility. But it can break if the member was previously hidden and now becomes visible, potentially causing name conflicts. The key is to consider the impact on existing subclasses.

In a library, private protected is a good choice for members that are part of the inheritance contract but only for classes in the same assembly. This prevents external consumers from depending on those members, giving you the freedom to change them in future versions without breaking external code. On the other hand, protected members are part of the public API for subclasses, so they must be maintained with care.

Practical Decision Table

The following table summarizes which access modifiers are visible to a derived class under different assembly relationships:

ModifierSame assembly, derived classOther assembly, derived classSame assembly, non-derived
publicYesYesYes
protectedYesYesNo
internalYesNoYes
protected internalYesYesYes (same assembly only)
private protectedYesNoNo
privateNoNoNo

Use this table when you are deciding which modifier to apply. The key distinction is whether you need to support derived classes in other assemblies. If you are building a framework that will be extended externally, protected is the standard choice. If you are building an internal library with a controlled set of subclasses, private protected gives you more flexibility.

Final Example: Combining Access Modifiers with Override

Here is a complete example that demonstrates a realistic use of protected internal and private protected in a class hierarchy:

// Assembly: DataAccess public abstract class Repository { protected internal string ConnectionString { get; set; } private protected string CacheKey { get; set; } protected abstract object GetData(); } public class SqlRepository : Repository { protected override object GetData() { // Can access ConnectionString and CacheKey return new object(); } } // Assembly: App (references DataAccess) public class CustomRepository : Repository { protected override object GetData() { // Can access ConnectionString, but NOT CacheKey return new object(); } }

In this design, ConnectionString is available to any derived class, even in other assemblies, because it is protected internal. CacheKey is only available to derived classes in the same assembly, which prevents external subclasses from depending on an internal caching detail. This separation allows the library to change CacheKey without breaking external consumers.

Understanding these rules helps you design inheritance hierarchies that are both flexible and maintainable. The access modifier you choose determines the contract you offer to derived classes, and that contract is hard to change later. Choose deliberately based on how you intend the class to be extended.

c# access modifiers in inheritance: Practical Usage and Code | RYUSLOG DEV