Back to Blog
C#

C# Access Modifier Scope: How Visibility Works

c# access modifier scope: Understand how C# access modifiers control member visibility and scope, with practical examples and guidance for choosing the right level.

access modifiersC# visibilityencapsulationC# scopeaccessibility levels
Diagram showing C# access modifier scope from public to private, with a class and its members

c# access modifier scope requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you declare a class, method, property, or field in C#, you must decide how widely it should be accessible. The access modifier you choose determines the scope of that member: which parts of your codebase can see and use it. Getting this wrong leads to either over-exposed APIs or code that is unnecessarily restrictive and hard to reuse. This article explains the six access modifiers in C#, how their scope behaves in nested types and inheritance, and how to pick the right one for a given situation.

The Six Access Modifiers and Their Scope

C# defines six access modifiers: public, private, protected, internal, protected internal, and private protected. Each one defines a distinct accessibility scope. The following table summarizes them before we examine each in detail.

ModifierScope
publicNo restrictions; accessible from any code that can see the containing type
privateOnly within the same type (including nested types)
protectedWithin the same type or any derived type
internalWithin the same assembly
protected internalWithin the same assembly or any derived type in another assembly
private protectedWithin the same assembly and only in derived types

Note that protected internal is a union of protected and internal, while private protected is an intersection. The distinction matters when you have inheritance across assemblies.

Public and Private: The Two Extremes

The public modifier makes a member accessible from anywhere that can reference the containing type. For example, a public method on a class can be called from any other class in the same assembly or from a different assembly that references it. This is the default for top-level types (classes, structs, enums) when no modifier is specified, though it is clearer to write public explicitly.

public class Calculator { public int Add(int a, int b) => a + b; }

Here, Add is part of the class's public contract. Any consumer of Calculator can call it. The private modifier is the opposite: the member is only accessible within the body of the same type. A private field or method is an implementation detail that cannot be touched from outside.

public class Counter { private int _count; public void Increment() => _count++; public int Current => _count; }

The _count field is private; it cannot be read or written directly from outside Counter. This is the foundation of encapsulation: you control how the internal state changes. Using private for fields that should not be exposed is a basic practice, but scope decisions get more nuanced when inheritance and assemblies are involved.

Protected and Internal: Scope with Inheritance and Assemblies

The protected modifier allows a member to be accessed from the containing type and from any type that derives from it. This is useful when you want to expose a behavior or a field only to subclasses, while keeping it hidden from unrelated code.

public class Animal { protected string Name { get; set; } } public class Dog : Animal { public void SetName(string name) => Name = name; }

Dog can access Name because it inherits from Animal, but a class that is not derived from Animal cannot. Note that protected does not limit access to the same assembly; a derived class in another assembly can still access a protected member.

The internal modifier restricts access to the same assembly. This is commonly used for internal APIs that should not be exposed to consumers of the library but need to be shared among classes within the assembly.

internal class Configuration { internal string ConnectionString { get; set; } }

If you are building a class library, internal members are invisible to code that references the library. This is a powerful tool for hiding implementation details that are not part of the public contract.

Protected Internal and Private Protected: Combining Rules

The protected internal modifier is a union: the member is accessible from the same assembly, or from any derived type in another assembly. This can be confusing because it is not an intersection. A member declared protected internal is effectively internal plus protected. For example:

public class Base { protected internal void Helper() { } }

Any code in the same assembly can call Helper, and any derived class in another assembly can also call it. If you want a member that is only accessible in derived types and only within the same assembly, you need private protected.

public class Base { private protected void InternalHelper() { } }

InternalHelper is accessible only from a derived type that is also in the same assembly. This is useful when you are building a library and want to allow subclassing only within your own assembly, preventing external subclasses from seeing certain members.

Default Access Modifiers and Scope for Types

When you omit an access modifier, C# applies a default. For top-level types (classes, structs, enums), the default is internal. For members of a class or struct, the default is private. For members of an interface, the default is public (though in modern C# you can explicitly add modifiers to interface members). Nested types follow the same rule: if you declare a class inside another class without a modifier, it is private.

class Outer { class Inner { } // private by default }

Understanding defaults is important because they affect the scope of your code even when you do not write a modifier. If you are creating a helper class that is only used within a single file, making it internal or even private nested is often the right choice to avoid expanding its scope unnecessarily.

How Scope Affects Maintainability and API Design

The choice of access modifier directly impacts how maintainable your code is. A public member is a commitment: once you expose it, changing or removing it can break consumers. Keeping members private or internal gives you the freedom to refactor implementation details without affecting external code. For example, if you have a method that is only called from within the same class, make it private. If it is used by several classes in the same assembly but not by consumers, make it internal. This reduces the surface area of your API and makes future changes safer.

In a class library, you should start with the most restrictive modifier that works. If a member does not need to be public, do not make it public. This is not about hiding everything, but about being deliberate about what you expose. A common mistake is making everything public to avoid thinking about scope, which leads to a bloated API and more code that depends on internal behavior.

Common Mistakes and Edge Cases with Access Modifier Scope

One frequent error is assuming that protected also limits access to the same assembly. It does not. A derived class in a different assembly can access a protected member, as long as it inherits from the base class. Another mistake is using protected internal when you actually meant private protected. The union semantics of protected internal are broader than many developers expect. If you want a member to be accessible only in derived classes and only within the same assembly, private protected is the correct choice.

Another edge case is the accessibility of a member in relation to the accessibility of its containing type. A public member cannot be exposed from an internal class, because the containing type is less accessible. For instance, if you have an internal class with a public method, the method is effectively internal because it cannot be called from outside the assembly. The compiler enforces that the accessibility of a member cannot exceed the accessibility of its containing type.

internal class InternalClass { public void PublicMethod() { } // effectively internal }

This is not an error, but it can be misleading. If you intend a member to be public, the containing type must also be public. When designing your types, consider the combined accessibility of the type and its members.

Choosing the Right Access Modifier for Your Scenario

There is no single rule that fits every case, but you can make a decision based on the intended usage. If a member is part of the public API that external consumers will call, use public. If it is only used internally within the same class, use private. If it is shared among classes in the same assembly but not exposed externally, use internal. If you are building a base class and want to allow subclasses to override or access a member, use protected. If you need to allow access from the same assembly and from derived classes in other assemblies, use protected internal. If you want to restrict access to derived classes only within the same assembly, use private protected.

A practical approach is to start with the most restrictive modifier and widen it only when you have a concrete need. This keeps your API surface minimal and reduces the risk of accidental coupling. When you later find that a member must be accessible from a wider scope, you can change the modifier, but doing so is a breaking change for public members. Therefore, it is better to start restrictive and relax later than to start permissive and try to tighten it.

c# access modifier scope: Practical Usage and Code Examples | RYUSLOG DEV