📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What access modifiers does C# have, and how do you use them to design a clean API? Explain public, private, protected, internal, protected internal, private protected and file, the default accessibility of each declaration, and InternalsVisibleTo.

C# access modifiers including private protected and file, defaults per declaration, accessor-level modifiers, InternalsVisibleTo, and using accessibility to keep API surface small.

Junior Theory Beginner access-modifiers api-design csharp encapsulation internal oop

🎯 Key points

  • public, private, protected and internal are the four basic levels
  • protected internal is the union (derived OR same assembly); private protected is the intersection (derived AND same assembly)
  • file (C# 11) limits a type to its source file, mainly for source generators
  • Top-level types default to internal; class and struct members default to private; interface and enum members default to public
  • Property accessors can be more restrictive than the property (`get; private set;`)
  • Public signatures cannot expose less accessible types (inconsistent accessibility)
  • InternalsVisibleTo opens internals to named assemblies such as test projects
  • Access modifiers are a design tool, not a security boundary; reflection and UnsafeAccessor bypass them

⏱ 30-second answer

C# has seven accessibility levels. public: anyone. private: only the containing type (the default for members). protected: the type and derived types. internal: the same assembly (the default for top-level types). protected internal: derived types or the same assembly. private protected (C# 7.2): derived types and in the same assembly. file (C# 11): a type visible only in its source file, mainly for source generators. You can also restrict a single accessor (public string Name { get; private set; }). [InternalsVisibleTo] opens internals to named assemblies such as tests. Access modifiers are a design tool, not a security boundary: reflection can bypass them.

🗣 Full answer (senior)

How I would answer this in an interview

Access modifiers control who can see and use a type or member. They're the main tool for encapsulation at every scale: inside a class, across an assembly, and at the public API boundary. I'd go through all seven, the defaults, InternalsVisibleTo, and the principle I follow: start as restrictive as possible and widen only when needed.

1. The modifiers

Modifier Accessible from Typical use
public Anywhere The deliberate API surface
private The containing type only (including nested types) Implementation details, state
protected The containing type and derived types (any assembly) Extension hooks for subclasses
internal Any code in the same assembly Types and members shared within a project but hidden from consumers
protected internal Same assembly OR derived types anywhere Rare; framework extension points also used internally
private protected (C# 7.2) Derived types in the same assembly only Hooks for internal subclasses without exposing them publicly
file (C# 11, types only) The same source file Helpers, especially in source generators, avoiding name clashes

protected internal is a union (either condition); private protected is an intersection (both conditions).

2. Defaults

Declaration Default
Top-level class, struct, record, interface, enum, delegate internal
Nested type private
Class/struct/record members private
Interface members public (can be explicitly restricted since C# 8)
Enum members public (always)
Explicit interface implementations No modifier allowed (accessible via the interface)

Being explicit is clearer — analysers (IDE0040) can enforce "always specify accessibility".

3. Designing a clean API with them

  • Public surface = contract. Everything public in a library is something you must support and can't change without breaking consumers. Keep it minimal: the types and methods consumers need.
  • internal by default for application projects: a typical solution splits Domain, Application, Infrastructure and API projects. Infrastructure implementations (SqlOrderRepository, StripePaymentGateway) can be internal, exposed only through an AddInfrastructure(this IServiceCollection) extension method. Consumers can't depend on implementation details.
  • private state, public operations inside types (encapsulation).
  • private set / init to control mutation of properties.
  • protected sparingly — every protected member is a contract with subclasses in other assemblies.
  • Seal public classes not designed for inheritance.
  • file for helper types that should never leak, including generator output.

4. InternalsVisibleTo

<ItemGroup>
  <InternalsVisibleTo Include="Ordering.Tests" />
</ItemGroup>

Makes internal members visible to a named "friend" assembly (signed assemblies need the public key).

  • Good use: letting a test project test internal types without making them public; sharing internals between tightly coupled assemblies of the same product.
  • Overuse: testing private-ish implementation details, making tests brittle. Prefer testing through the public API, and use InternalsVisibleTo for internal components that are meaningful units on their own (parsers, calculators).
  • Mocking libraries (Moq/Castle) need InternalsVisibleTo("DynamicProxyGenAssembly2") to mock internal interfaces.

5. A production example

A shared "Common" library had every type public by default, and other teams started depending on its internal helpers (a SqlRetryHelper, a JsonUtil). When we needed to change those helpers, we couldn't without breaking five services. We introduced a rule: new types are internal sealed unless there's a reason; we added a public API analyser (Microsoft.CodeAnalysis.PublicApiAnalyzers with PublicAPI.Shipped.txt) so any change to the public surface shows up in code review; and we gradually deprecated public helpers with [Obsolete] before internalising them in the next major version.

🔍 What the interviewer is checking

Checks accuracy on all seven levels and the judgement to keep APIs small.

Green flags: correct union/intersection explanation; defaults for each declaration kind; accessor modifiers; file types; InternalsVisibleTo; not a security boundary; internal-by-default for libraries.

Red flags: mixes up protected internal and private protected; thinks the default for classes is private or public; everything public; protected fields.

📚 Learn in depth

1. Enforcement: compiler and runtime

  • The C# compiler enforces accessibility at compile time.
  • The CLR also enforces it at JIT/load time ("member access checks"): IL that calls a private method in another type fails with MethodAccessException — unless reflection with BindingFlags.NonPublic is used, which bypasses compile-time checks but not (in full trust) the ability to call. Since .NET Core, reflection on private members works; trimming/AOT may remove members accessed only via reflection.
  • UnsafeAccessorAttribute (.NET 8) lets code access private members of other types without reflection, efficiently — intended for serialisers and test helpers. It's a deliberate escape hatch; don't use it to work around poor API design.

2. Metadata representation

IL flags: public, private, family (protected), assembly (internal), famorassem (protected internal), famandassem (private protected), plus nested variants for nested types. The file modifier is a C# concept implemented by giving the type a mangled, unique name (<FileName>F..__Helper) and internal accessibility.

3. Accessibility domains and consistency rules

A member's effective accessibility is the intersection of its own and its containing type's: a public method on an internal class is effectively internal. The compiler enforces consistency: a public method can't expose an internal type in its signature (CS0050/CS0051), because consumers couldn't use it.

4. protected access rules

A derived class can access a protected member only through an instance of its own type (or further derived), not through any base-typed instance:

class Base { protected int X; }
class Derived : Base
{
    void M(Base b, Derived d) { _ = d.X; /* ok */ _ = b.X; /* CS1540 error */ }
}

This prevents a subclass from reaching into sibling subclasses' protected state.

5. Public API management

  • PublicApiAnalyzers: track public surface in text files; any addition or removal produces a diagnostic until the file is updated — changes become visible in PRs.
  • Package validation (<EnablePackageValidation>true</EnablePackageValidation>) compares against a baseline package version to detect breaking changes.
  • [EditorBrowsable(EditorBrowsableState.Never)] hides public members from IntelliSense when they must be public for technical reasons.
  • [Obsolete] with a diagnostic ID for staged removals.

6. Accessibility and testing strategy

Testing through public APIs keeps tests resilient to refactoring. Expose internals to tests when a component is complex and stable enough to deserve its own tests (e.g. a tax rules engine) but shouldn't be public. Avoid making things public just for tests — it permanently widens the contract.

Try it yourself

  1. Make an infrastructure project's implementations internal and expose only a DI registration extension.
  2. Add Microsoft.CodeAnalysis.PublicApiAnalyzers to a library and see the diagnostics.
  3. Try accessing a protected member through a base-typed reference in a derived class.
  4. Create a file class in two files with the same name and confirm no conflict.

💻 Code

// Library: public surface is small, everything else internal
public sealed class OrderService(IOrderStore store)
{
    public async Task<OrderId> PlaceAsync(PlaceOrder cmd, CancellationToken ct)
    {
        var order = OrderFactory.Create(cmd);   // internal helper
        await store.SaveAsync(order, ct);
        return order.Id;
    }
}

internal static class OrderFactory
{
    internal static Order Create(PlaceOrder cmd) => new(OrderId.New(), cmd.CustomerId, cmd.Lines);
}

public class Account
{
    public decimal Balance { get; private set; }          // read anywhere, write here only
    protected void Credit(decimal amount) => Balance += amount;   // for subclasses
    private protected void Audit(string what) { }        // subclasses in THIS assembly only
    protected internal virtual void OnClosed() { }        // subclasses OR same assembly
}

// File-local type: no clash with other files' Helper types
file sealed class Helper
{
    public static string Normalise(string s) => s.Trim().ToUpperInvariant();
}
<!-- Orders.csproj -->
<ItemGroup>
  <InternalsVisibleTo Include="Orders.Tests" />
</ItemGroup>

⚠ Common mistakes

1. Making everything public

What happens: IDE templates or habit produce public types and members everywhere. Why it is wrong: the whole implementation becomes a contract consumers depend on. Do instead: default to internal sealed and private; widen deliberately.

2. Public setters on domain state

What happens: public decimal Balance { get; set; }. Why it is wrong: callers can bypass invariants. Do instead: private set or init, with methods that enforce rules.

3. Overusing protected

What happens: fields and helpers marked protected "for subclasses". Why it is wrong: creates a large inheritance contract across assemblies. Do instead: keep state private; expose a few deliberate protected hooks or use private protected.

4. Abusing InternalsVisibleTo

What happens: many assemblies are friends, or tests target tiny private details. Why it is wrong: blurs boundaries and makes tests brittle. Do instead: test through public behaviour; grant friend access sparingly.

5. Making members public for tests

What happens: private methods made public to assert on them. Why it is wrong: permanently widens the API. Do instead: test via public behaviour or extract a separate internal type.

6. Confusing protected internal and private protected

What happens: choosing the wrong one and exposing members to external subclasses. Why it is wrong: protected internal is broader (union), not narrower. Do instead: remember: protected internal = either; private protected = both.

7. Inconsistent accessibility in signatures

What happens: public methods returning internal types. Why it is wrong: compile errors, or pressure to make internal types public. Do instead: design the public surface with public types only, keeping implementations internal.

8. Not tracking public API changes in libraries

What happens: breaking changes slip into NuGet packages. Why it is wrong: consumers break on upgrade. Do instead: use PublicApiAnalyzers and package validation.

🏛 Senior insight: trade-offs

Seniors use accessibility to control coupling, especially in shared libraries and modular monoliths.

Context Senior default
Shared library internal by default, public API tracked by the analyser
Modular monolith Each module exposes only contracts; internals hidden; architecture tests enforce it
Inheritance hooks protected methods, never fields; private protected to keep hooks inside the assembly
Test access InternalsVisibleTo sparingly; prefer testing through the public surface
Generated code file types to avoid name clashes

They see every public keyword as a cost: it is a promise to keep that member stable.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says modifiers enforce encapsulation Why: Checks invariant-based encapsulation beyond keywords
    How to answer

    Encapsulation means an object owns its state and only lets it change through behaviour that keeps it valid. In C# you keep fields private, expose read access through properties (often get-only or private set), and offer methods like Withdraw(amount) that check rules before changing state, instead of a public setter on Balance. Collections are exposed as IReadOnlyList<T> with Add... methods. The benefit is that invariants ("balance never goes below the overdraft limit") are enforced in one place, so callers cannot put the object into an invalid state, and the internals can change without breaking callers.

  2. When: Candidate mentions default private for nested types Why: Probes nested type access rules
    How to answer

    A nested type is a class, struct, record, interface, enum or delegate declared inside another type. It is a member of the outer type, so its default accessibility is private, and it can access all members of the outer type, including private ones. Unlike a Java inner class, a C# nested class has no implicit reference to an outer instance: to use outer instance members you must pass the outer object in. Nested types are good for private implementation details (an enumerator, a comparer, a state object, a builder) and for tightly coupled helpers. Public nested types are usually discouraged (CA1034) because callers must write Outer.Inner and the type cannot be moved without breaking them.

🛠 Real-world problems: how you handle them

Shrinking a shared library's public surface to stop breaking changes
Situation
A shared domain library used by [12] services had nearly every class public, including helpers and EF configurations.
Signals
Each internal refactor produced breaking changes; services pinned old versions and fell [several] releases behind.
Investigation
Used the public API analyser to list the public surface ([900+] members) and searched consumer repositories to find what was really used (about [80] members).
Resolution
Made everything else `internal` in a major version, documented the supported API, and added `InternalsVisibleTo` for the library's own tests.
Prevention
Public API analyser in CI so any new public member must be added deliberately to `PublicAPI.Unshipped.txt` and reviewed.

How to tell it (STAR)

**Situation:** our shared library exposed over [900] public members, so every refactor broke someone. **Task:** make internal changes safe without blocking teams. **Action:** I measured real usage, kept about [80] members public, made the rest internal in a major release, and added the public API analyser so additions are reviewed. **Result:** the next [six] releases had no breaking changes, and services caught up to the latest version within a sprint.

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me