Back to Blog
C#

C# Access Modifier Scope: How Visibility Works

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

An access modifier controls the visibility of a type or member in C#. When you declare a class, method, property, or field, the modifier you choose determines its scope: which parts of your codebase can see and use it. Getting this wrong can produce over-exposed APIs or code that is unnecessarily restrictive and hard to reuse. This article explains the main access modifiers, how their scope behaves in nested types and inheritance, and how to choose the right one.

The Main Access Modifiers and Their Scope

C# defines these common access modifiers: public, private, protected, internal, protected internal, private protected, and file. The following table summarizes their scope.

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
fileTop-level types only; accessible from the current source file (C# 11 and later)

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. The file modifier is described in its own section below.

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 public class can be called from any other class in the same assembly or from a different assembly that references it. public is not the default for top-level types; the default is internal. Write public explicitly when you intend the member to be part of the type's public contract.

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 referenced 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.

The file Modifier (C# 11)

C# 11 adds a file access modifier for top-level types. A file class, struct, interface, enum, or delegate can be used only in the source file where it is declared. You cannot apply file to nested types, and you cannot combine it with another access modifier on the same type. This is useful for implementation helpers that multiple parts of the same file need but should not become part of the assembly's visible API.

file class FileScopedHelper { public string Normalize(string value) => value.Trim(); }

Outside the file, FileScopedHelper is not visible, even to other code in the same assembly.

Default Access Modifiers and Scope for Types

When you omit an access modifier, C# applies a default. For top-level types (classes, structs, interfaces, enums, and delegates), 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). For nested types in a class or struct, the same default applies: a nested class without a modifier 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, internal is often the right choice; a file-scoped class is an alternative when you want it visible only to that exact source file.

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 relationship between a member's accessibility and its containing type's accessibility. A public member declared in an internal class is effectively internal because code outside the assembly cannot access the containing type. The compiler enforces that a member's accessibility cannot exceed the accessibility of its containing type.

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

This code compiles, 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.

With C# 11, do not forget that file applies only to top-level types and cannot be combined with another access modifier. It is useful when a helper type is needed in only one source file.

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. For a top-level helper type that is used only in one source file, use file in C# 11 or later.

A practical approach is to start with the most restrictive modifier that works 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 Examples and Best Practices | RYUSLOG DEV