Access modifiers control who (which code) can see and use types and members. Proper use improves encapsulation, reduces bugs, and makes APIs safer and easier to evolve. This article covers each modifier, defaults, real-world scenarios, code snippets, and best practices.
What are Access Modifiers?
Access modifiers (sometimes called accessibility levels) limit visibility of types (classes, structs, interfaces) and their members (fields, properties, methods, nested types, constructors). They are the primary tool for encapsulation in object-oriented design: hide implementation details and expose only the necessary interface.
C# provides these main modifiers:
-
public
-
private
-
protected
-
internal
-
protected internal
-
private protected
(C# 7.2+)
Quick Cheat Sheet
Modifier | Who can access |
---|---|
public | Any code anywhere (any assembly) |
private | Only inside the containing type |
protected | The containing type and derived types (any assembly) |
internal | Any code in the same assembly |
protected internal | Derived types or same assembly (union) |
private protected | Derived types in the same assembly only (intersection) |
Defaults: Top-level (namespace) types default to internal
. Members inside classes default to private
. Interface members are implicitly public
(traditional rule; newer C# versions add advanced options for default/interface implementations).
Each Modifier — Detailed Explanation + Code Examples
public
Exposes an API element to everyone. Use for types and members you want external consumers to use.
// Public DTO used across services
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; } // public property – readable/writable by callers
}
Real use: Web API controllers, DTOs, library public APIs, SDK surface.
private
Most restrictive. Use it to hide implementation details inside a class.
public class BankAccount
{
private decimal _balance; // only this class can access
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException();
_balance += amount;
}
public decimal GetBalance() => _balance;
}
Real use: Backing fields, helper methods used only by the class, internal caching.
protected
Visible to the declaring class and any derived classes (regardless of assembly).
public class BaseLogger
{
protected void Log(string message) { /* write to base log */ }
}
public class FileLogger : BaseLogger
{
public void LogError(string msg)
{
Log($"ERROR: {msg}"); // can use protected member
}
}
Real use: Base classes that expose extension points to subclasses (template methods, hooks).
internal
Visible to any code in the same assembly. Good for grouping implementation details per component or library.
internal class QueryOptimizer
{
// Implementation used only within the assembly
}
Real use: Implementation classes inside a NuGet package, non-public helpers, layering inside a single assembly.
Tip: Use [assembly: InternalsVisibleTo("MyProject.Tests")]
to grant a test assembly access to internal
members for unit testing.
// In AssemblyInfo.cs or top of a .cs file
[assembly: InternalsVisibleTo("MyProject.Tests")]
protected internal
Union: accessible either from derived types (any assembly) or from any code in the same assembly.
public class PluginBase
{
protected internal virtual void Initialize() { /* default init */ }
}
Real use: Library extension points where implementers (derived types) or code inside the same assembly should be able to call a member.
private protected
Intersection: accessible only to derived types that are in the same assembly. Use when you want to keep inherited access restricted to the current assembly.
public class CoreComponent
{
private protected void InternalHook() { /* only derived classes in same assembly */ }
}
Real use: Tight encapsulation for inheritance scenarios inside a single assembly (common in internal frameworks).
Real-World Scenarios and Examples
1) ASP.NET Core: Controllers and Dependency Injection
Controllers must be public so the framework can discover them. Services can be internal
if only used within the app.
// Controller must be public
public class OrdersController : ControllerBase
{
private readonly IOrderService _service; // private field
public OrdersController(IOrderService service) => _service = service;
[HttpGet("{id}")]
public ActionResult<OrderDto> Get(int id) => _service.GetOrder(id);
}
2) Encapsulation: Public Getter, Private Setter
Expose read access but protect mutation.
public class User
{
public string Email { get; private set; } // callers can read, class controls writes
public void ChangeEmail(string newEmail)
{
/* validation */
Email = newEmail;
}
}
3) Library Design: Public API vs Internal Implementation
Expose a small, stable public API and keep the messy details internal.
MyLibrary (assembly)
├─ public: ApiClient, Models
└─ internal: HttpTransport, Caching, Helpers
4) Unit Testing Internals
Grant tests access to internal
classes:
// In production project (AssemblyInfo.cs)
[assembly: InternalsVisibleTo("MyLibrary.Tests")]
5) Cross-Assembly Inheritance: protected internal
vs private protected
-
Use
protected internal
if you want derived types outside the assembly to access members. -
Use
private protected
to restrict derived-type access to the same assembly.
Practical Guidance — How to Choose an Access Modifier
-
Start strict, open only if needed: Prefer
private
→protected
→internal
→public
. Least privilege is safer. -
Public surface = contract: Anything
public
is a commitment — minimize public API to what you truly support. -
Use
internal
for implementation details to reduce breaking changes when you refactor. -
Use
protected
for extension points intended for inheritance. -
Prefer properties over public fields. Fields should rarely be
public
. -
Use
private
constructors to control instantiation (singletons, factories). -
Use
InternalsVisibleTo
carefully when you must test internals — document it.
Common Pitfalls & Anti-Patterns
-
Making fields
public
instead of exposing properties. -
Overusing
public
for convenience — increases coupling. -
Exposing mutable internal state (e.g.,
public List<T>
) — prefer read-only or defensive copies. -
Assuming
internal
equals private —internal
exposes to the entire assembly (package consumers might depend on it).
Small Pattern Examples
Singleton with private
ctor
public class Logger
{
private static readonly Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
}
Public API with internal helpers
public class PaymentProcessor
{
private readonly PaymentValidator _validator = new PaymentValidator(); // internal helper
public bool Process(Payment p) => _validator.IsValid(p) && /* process */ true;
}
internal class PaymentValidator
{
public bool IsValid(Payment p) { /* ... */ return true; }
}
Summary — Best Practices
-
Use the least permissive modifier that still allows necessary functionality.
-
Keep public surface minimal and intentional.
-
Use
internal
to keep implementation details inside an assembly. -
Use
protected
/protected internal
for well-documented extension points. -
Apply
InternalsVisibleTo
only when needed for tests/trusted friend assemblies.
No comments:
Post a Comment