Showing posts with label access modifiers in C#. Show all posts
Showing posts with label access modifiers in C#. Show all posts

Friday, October 3, 2025

Access Modifiers in C# — Complete Guide (with Real-World Examples)

 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

ModifierWho can access
publicAny code anywhere (any assembly)
privateOnly inside the containing type
protectedThe containing type and derived types (any assembly)
internalAny code in the same assembly
protected internalDerived types or same assembly (union)
private protectedDerived 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

  1. Start strict, open only if needed: Prefer privateprotectedinternalpublic. Least privilege is safer.

  2. Public surface = contract: Anything public is a commitment — minimize public API to what you truly support.

  3. Use internal for implementation details to reduce breaking changes when you refactor.

  4. Use protected for extension points intended for inheritance.

  5. Prefer properties over public fields. Fields should rarely be public.

  6. Use private constructors to control instantiation (singletons, factories).

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

Blog Archive

Don't Copy

Protected by Copyscape Online Plagiarism Checker

Pages