Nested classes in C#: access to outer private members, no implicit outer instance, private default accessibility, generics, BCL examples and design guidelines.
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.
A nested type is a class, struct, record, interface, enum or delegate declared inside another type. In C#, a nested class is like Java's static nested class, not an inner class: it has no implicit reference to an instance of the outer type. I'd cover what it can access, default accessibility, and when nesting is good or bad design.
public sealed class Order
{
private decimal _discount;
public sealed class Line // nested type
{
public decimal Price(Order order) => Amount - order._discount; // needs an Order passed in
public decimal Amount { get; init; }
}
}
var line = new Order.Line { Amount = 10m }; // created without any Order instance
Outer.this implicitly. C# has no such capture — if the nested type needs an outer instance, you pass it explicitly (constructor or parameter).private ones — static members directly, instance members through an outer instance reference.Outer<T>.Inner can use T.This privileged access is the main reason to nest: a helper that needs the outer type's private state, without exposing that state publicly.
| Declaration | Default accessibility |
|---|---|
| Top-level type | internal |
| Nested type | private |
| Class/struct members | private |
| Interface members | public |
A nested type can be given any accessibility (public, protected, internal, private protected...). Public nested types are referenced as Outer.Inner.
List<T>.Enumerator), state-machine helpers, comparers, builders used only by the outer type.Builder that sets private fields of the object it builds, or an iterator over private storage.public abstract record PaymentResult
{
private PaymentResult() { } // only nested types can derive
public sealed record Approved(string AuthCode) : PaymentResult;
public sealed record Declined(string Reason) : PaymentResult;
public sealed record Failed(Exception Error) : PaymentResult;
}
The private constructor plus nested derived records makes the hierarchy closed: nobody outside can add a new subtype. That's a common pattern until C# gets native union types.
HttpClientFactoryOptions-style nesting is used sparingly in the BCL).Outer.Inner everywhere, and the design guidelines recommend against public nested types except in specific cases (CA1034).A.B.C.D is hard to read and discover.A payments module returned results as object plus status codes, and callers used switch on strings. We modelled outcomes as a closed nested-record hierarchy (PaymentResult.Approved/Declined/Failed) with a private base constructor. API mappers switched on the types with pattern matching, and because nobody outside the file could add a new result type, reviewers knew the set of cases was complete. When we did add RequiresAction for 3-D Secure, it was in one place and the compiler's discard-arm warnings plus tests guided updates to every switch.
Checks precise knowledge of nested type rules and design judgement about public surface area.
Green flags: private by default; access to outer privates; no implicit outer instance and the Java contrast; generic nesting; BCL enumerator example; CA1034 and when public nesting is acceptable; returning a private nested class as an interface.
Red flags: thinks nested classes have an outer this; uses nesting for namespacing; cannot name a good use case.
Nested types are stored with a NestedClass table entry linking them to their enclosing type. Their full metadata name uses +: MyApp.Order+Line (that's what you see in Type.FullName, stack traces and reflection). In C# source you write Order.Line. typeof(Order.Line).IsNested is true; DeclaringType returns Order.
The CLR enforces accessibility, and nested types are considered "inside" their enclosing type for private access. That's why a nested class can access private fields of the outer type — the access check treats the nested type's code as part of the outer type. For nested-in-generic types, Outer<int>.Inner and Outer<string>.Inner are distinct types.
public abstract class Shape
{
private Shape() { }
public sealed class Circle(double r) : Shape { public double R { get; } = r; }
public sealed class Square(double s) : Shape { public double S { get; } = s; }
}
Because the base constructor is private, only nested types can call it, so the set of subclasses is fixed. Pattern matching still requires a discard arm (the compiler doesn't treat the hierarchy as closed), but the design communicates and enforces closure. Some analysers and libraries (e.g. Dunet, OneOf) build on similar patterns.
public class Cache<TKey, TValue>
{
private sealed class Entry { public TValue Value = default!; public DateTimeOffset Expires; }
private readonly Dictionary<TKey, Entry> _entries = new();
}
Entry automatically uses TValue. Each closed generic gets its own Entry type. This is neat for private helpers; for public nested types in generic classes, callers must write Cache<string, int>.Entry, which is clunky.
List<T>.Enumerator, Dictionary<TKey,TValue>.KeyCollection — public nested types tightly coupled to their outer type (exposed for performance and foreach pattern binding).Environment.SpecialFolder — a nested enum.HttpClientHandler internals use many private nested helpers.The guidelines allow public nested types when they're rarely referenced by name or tightly bound to the outer type (enumerators), and discourage them for commonly used types.
C# 11's file access modifier (file class Helper) restricts a top-level type to its source file — another way to hide helpers without nesting, especially useful for source generators to avoid name collisions. Use nesting when the helper needs the outer type's private members; use file when it just needs to be invisible elsewhere.
typeof(Order.Line).FullName and observe the +.file-scoped class and compare.public sealed class RateLimiter(int permits)
{
private int _available = permits;
private readonly Lock _lock = new(); // System.Threading.Lock (.NET 9+)
public IDisposable? TryAcquire()
{
lock (_lock)
{
if (_available == 0) return null;
_available--;
return new Lease(this); // callers only see IDisposable
}
}
// Private nested type: an implementation detail that can change freely
private sealed class Lease(RateLimiter owner) : IDisposable
{
private int _disposed;
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
lock (owner._lock) owner._available++; // reads outer PRIVATE fields
}
}
}
// Generic outer type: nested type uses outer type parameters
public sealed class LruCache<TKey, TValue>(int capacity) where TKey : notnull
{
private sealed class Node(TKey key, TValue value)
{
public TKey Key { get; } = key;
public TValue Value { get; set; } = value;
}
private readonly Dictionary<TKey, LinkedListNode<Node>> _map = new(capacity);
private readonly LinkedList<Node> _order = new();
}
What happens: using outer instance members from a nested class without a reference. Why it is wrong: C# nested types have no implicit outer instance. Do instead: pass the outer instance explicitly when needed.
What happens: Order.Line, Order.Status used throughout the codebase.
Why it is wrong: verbose references and poor discoverability (CA1034).
Do instead: make commonly used types top-level in a sensible namespace.
What happens: static classes containing many unrelated nested types for organisation. Why it is wrong: misuse of types for grouping; awkward references. Do instead: use namespaces and folders.
What happens: three or more levels of nested types. Why it is wrong: hard to read, navigate and test. Do instead: keep nesting to one level and extract types.
What happens: several nested classes modify the outer type's private fields. Why it is wrong: the outer type no longer controls its own invariants. Do instead: keep state changes in the outer type's methods; nested helpers call them.
What happens: a nested type is unexpectedly inaccessible from other classes.
Why it is wrong: confusion about the default private accessibility.
Do instead: specify accessibility explicitly.
What happens: result types are public abstract classes anyone can extend. Why it is wrong: callers can't rely on a fixed set of cases. Do instead: use a private base constructor with nested sealed subtypes.
What happens: a useful parser or calculator is nested privately.
Why it is wrong: can't reuse or test it directly.
Do instead: make it a top-level internal type.
Seniors use nested types to shrink public surface area and keep implementation details private.
| Use case | Senior choice |
|---|---|
| Helper used only by one class | Private nested class |
| Return an implementation behind an interface | Private nested class returned as the interface |
| Allocation-free enumeration | Public nested struct enumerator (BCL pattern) |
| Builder strongly bound to one immutable type | Public nested Builder is acceptable |
| Type used widely by callers | Top-level type |
They also mention that a private nested type can access the outer's private state, which is a controlled way to share internals without making them internal to the whole assembly.
List<T> wraps a T[] plus a _size count and a _version counter. When the array is full, Add allocates a new array twice as large (starting at 4) and copies, so Add is amortised O(1). Insert and Remove shift elements, so they are O(n). Every change increments _version; the enumerator checks it on each MoveNext and throws InvalidOperationException if the list changed. In hot paths, pre-size with the constructor or EnsureCapacity, and use CollectionsMarshal.AsSpan to work on the backing array without copying (but never add or remove while holding that span).
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.
How to tell it (STAR)
**Situation:** a refactor moved a public nested type in our shared rate-limiter library and broke [six] consuming services. **Task:** fix the break and stop the API from leaking implementation types. **Action:** I reverted in a patch, then in the next major version returned `IDisposable` and made the lease a private nested class. I enabled CA1034 and the public API analyser. **Result:** later changes to the lease implementation (including moving to the .NET 9 `Lock` type) needed no consumer changes.
Last updated 30 Oct 2026.