C# Access Modifiers: Syntax and Practical Usage
c# access modifiers: Learn how C# access modifiers control visibility and encapsulation, with examples of public, private, protected, internal, and their combinations.
When you declare a class, method, or field in C#, you must decide how visible it is to the rest of the application. That decision is made with access modifiers, and it shapes the API you expose to other code. C# access modifiers are not just a syntax detail; they define the contract between a component and its consumers. Getting them right keeps internal state safe, makes refactoring safer, and communicates intent clearly to other developers who read your code.
The Five Core Access Modifiers
C# provides five access modifiers that control where a member can be used from. The most commonly used are public, private, and protected. The other two, internal and protected internal, are equally important in larger solutions where assemblies and inheritance interact.
public– The member is accessible from any code in the same assembly or a referencing assembly.private– The member is accessible only within the containing type, including nested types.protected– The member is accessible within the containing type and by any derived type, even from another assembly.internal– The member is accessible from any code in the same assembly, but not from other assemblies.protected internal– The member is accessible from the same assembly or from derived types in other assemblies.
There is also a rarely used combination, private protected, which restricts access to derived types that are also in the same assembly. This is useful when you want to expose a member only to subclassing within the assembly where it is defined.
The most straightforward way to understand these modifiers is to look at a simple class.
public class Account { private decimal balance; protected decimal interestRate; internal string accountNumber; protected internal string bankName; public string Owner { get; set; } }
In this example, balance is available only inside the Account class, interestRate is available to derived classes, accountNumber is available within the assembly, bankName is available both within the assembly and to derived classes elsewhere, and Owner is public.
Default Accessibility in C#
Every type and member has a default access level if you do not specify one. The defaults are consistent and are worth memorizing because they influence how you structure code.
- Top-level types (classes, structs, enums) are
internalby default. - Members of a class or struct (methods, fields, properties, events) are
privateby default. - Members of an interface are
publicby default, because interfaces are meant to define public contracts. - Enum members are always
publicand cannot be modified.
Knowing these defaults prevents accidental overexposure. A class that is declared without a modifier is only visible within its own assembly, which is usually the intended behavior for implementation details. A private field without an explicit modifier is only visible within the class, which keeps internal state hidden.
How Access Modifiers Interact with Inheritance
Access modifiers have a direct impact on what derived classes can do. The protected modifier exists specifically to allow derived classes to access shared implementation details while keeping them hidden from unrelated code.
Consider a base class that provides a template method. The derived class needs to override a protected method, but external callers should only see the public operation.
public abstract class ReportGenerator { public void GenerateReport() { PrepareData(); FormatOutput(); } protected abstract void PrepareData(); protected virtual void FormatOutput() { // default formatting } }
Here, GenerateReport is the public API, while PrepareData and FormatOutput are protected hooks for derived classes. This pattern allows subclasses to customize behavior without exposing those customization points to the rest of the application.
When overriding a member, you cannot change its accessibility. If a method is protected in the base class, it must remain protected (or protected internal with care) in the derived class. Increasing visibility to public is not allowed, because that would break the Liskov substitution principle and the base class contract.
Accessibility Domains and Compile-Time Checks
The C# compiler uses the concept of an accessibility domain to determine whether a given reference to a member is legal. This domain is the set of program locations where that member can be accessed. The compiler checks every usage at compile time, so you get errors like Inconsistent accessibility: parameter type is less accessible than method when you try to expose a type that is less accessible than the method that uses it.
This happens frequently when you have an internal helper type and you try to use it as a parameter of a public method. The compiler disallows it because external callers would not be able to name the type, making the method unusable.
internal class DatabaseResult { /* ... */ } public class DataService { // Error: Inconsistent accessibility public DatabaseResult GetData() { return new DatabaseResult(); } }
The correct fix is to either make DatabaseResult public (if it makes sense as part of the API) or change the method to internal. This rule forces you to think about what your public API actually exposes.
Practical Guidance for Choosing the Right Modifier
A common mistake is to make everything public because it is the default in some other languages or because it is faster to type. In C#, being explicit about accessibility is important for maintainability.
Start with the narrowest accessibility that works. If a member is only used within a class, make it private. If it is needed only by derived types, use protected. If it is needed by other classes in the same assembly, use internal. Only use public when the member is truly part of the assembly's public API and intended for external consumers.
For example, a service class that coordinates repositories might expose a single public method, while its internal helper methods are private. This reduces the surface area and makes the public method the only point of interaction.
A practical rule: if you later need to change a private method to internal or protected, the change is localized and doesn't affect external consumers. But changing from public to private is a breaking change. Start narrow and widen only when the need is clear.
Access Modifiers for Types and Members
Access modifiers apply to types and to the members they contain, but the rules differ slightly. For a nested type, you can use private, protected, internal, or public. The accessibility of a nested type cannot exceed the accessibility of its containing type. A public nested class inside an internal class is effectively internal because the containing type is not accessible outside the assembly.
For members, the overall accessibility is the intersection of the member's own modifier and the accessibility of its containing type. If a class is internal, its public members are only accessible within the assembly.
This interaction matters when you are designing an API. You might have an internal class that is returned from an internal method, but you also have a public interface that the class implements. In that case, the interface's public methods are still accessible through the interface reference, even though the concrete class is internal.
Balancing Flexibility and Encapsulation
The protected internal modifier is often misunderstood. It combines protected and internal, meaning access is granted either if the code is within the same assembly or if the code is in a derived type in another assembly. This is not the same as protected and internal both being required; it is an OR condition.
In practice, use protected internal when you want to expose a member to the rest of your assembly for internal cooperation, but also want derived classes outside the assembly to be able to use it. A common scenario is a plugin-like architecture where the core assembly defines a base class, and external plugins need to call a hook method that is also used internally.
Common Mistakes and How to Avoid Them
One frequent error is using public fields when you should use properties with getters and setters. public fields expose the field directly, meaning you cannot later add validation or change the backing storage without breaking external code. Use private fields and public properties when the field is part of the API.
Another mistake is making members public just because they are used in tests. Testing often needs access to internal implementation. Instead of making a member public, you can use the InternalsVisibleTo attribute to expose internal members to a specific test assembly. This keeps your public API clean and still allows tests to verify internal behavior.
A third mistake is ignoring the default visibility of interfaces. Because interface members are always public, you cannot hide implementation details there. If an interface exposes a method, all implementers must be able to handle any call to that method. If you have implementation-specific logic that should not be part of the contract, keep it outside the interface.
Where Access Modifiers Matter at Runtime
Access modifiers are enforced by the compiler, but reflection can bypass them at runtime. Code that uses reflection can access private members, which is sometimes necessary for serialization or dependency injection frameworks. This means access modifiers are not a security boundary. They are a design and maintainability tool, not a protection against malicious or accidental bypass through reflection.
When you create a type that will be used by a dynamic proxy or an ORM, be aware that these tools often need access to protected or internal constructors and properties. If you design a class that is not accessible internally, you may need to explicitly configure the framework to allow non-public access. This is a practical concern that affects how you design domain entities and DTOs.
Final Technical Consideration: Compatibility and Refactoring
Changing an access modifier can have subtle effects on binary compatibility. If you have a public API that is consumed by external assemblies, changing a method from public to protected is a breaking change with respect to the public contract. However, changing a method from private to internal is not observable to external code, so it is safe for a library release.
During refactoring, the safest first move is to reduce accessibility while keeping the same behavior. If a method is used only internally, change it to internal and verify that no external code breaks. If it is only used by the class itself, change it to private. This incremental tightening makes the API surface smaller and gives you more freedom to change implementation details later.
At the end of the day, access modifiers are a way to express your design intentions. They tell other developers and the compiler which parts of your code are intended for general use and which parts are implementation details. Using them deliberately makes the codebase easier to maintain and less likely to have accidental dependencies on internals.