Understanding Nested Types
Nested types let you define classes, structs, interfaces, or enums inside another type. This creates a logical grouping where the inner type belongs exclusively to the outer type. You'll use them when a helper class makes sense only in the context of its container.
The key benefit is encapsulation. Nested types can access private members of their containing type, creating a tight coupling that's sometimes exactly what you need. This access works both ways, the outer type can also access private members of instances it creates of the nested type.
You'll learn when nested types improve your design versus when they create unnecessary complexity. We'll cover visibility rules, common use cases like state machines and builders, and practical examples you can use immediately.
Creating Nested Types
You define a nested type by placing its declaration inside another type's body. The nested type behaves like a regular type except it can access private members of the containing type. This creates an intimate relationship where the inner type is part of the outer type's implementation.
Nested types follow the same access modifiers as other members. A private nested type stays completely hidden, visible only to the containing type. A public nested type becomes accessible from outside using the syntax OuterType.InnerType, making the relationship explicit to callers.
public class LinkedList<T>
{
private Node? _head;
private int _count;
// Private nested type - only LinkedList can use it
private class Node
{
public T Value { get; set; } = default!;
public Node? Next { get; set; }
public Node(T value)
{
Value = value;
}
}
public void Add(T value)
{
var newNode = new Node(value);
if (_head == null)
{
_head = newNode;
}
else
{
var current = _head;
while (current.Next != null)
{
current = current.Next;
}
current.Next = newNode;
}
_count++;
}
public int Count => _count;
}
The Node class exists purely to support LinkedList's implementation. Making it private prevents other code from depending on this implementation detail. If you later change how the list stores items, no external code breaks because Node was never part of your public API.
Modeling State with Nested Types
Nested types excel at representing states in state machines. Each state becomes a nested class that encapsulates state-specific behavior. This pattern keeps all related states together and makes the state machine's structure clear from reading the outer class.
Using nested types for states makes it obvious these classes exist only to support the state machine. They access the machine's private fields directly, avoiding the need for callbacks or dependency injection patterns that would complicate simpler scenarios.
public class Connection
{
private State _currentState;
private string _connectionString = "";
public Connection()
{
_currentState = new DisconnectedState(this);
}
public void Open(string connectionString)
{
_currentState.Open(connectionString);
}
public void Close()
{
_currentState.Close();
}
public void ExecuteQuery(string query)
{
_currentState.ExecuteQuery(query);
}
// Base state class
private abstract class State
{
protected Connection Connection { get; }
protected State(Connection connection)
{
Connection = connection;
}
public abstract void Open(string connectionString);
public abstract void Close();
public abstract void ExecuteQuery(string query);
}
private class DisconnectedState : State
{
public DisconnectedState(Connection connection) : base(connection) { }
public override void Open(string connectionString)
{
Connection._connectionString = connectionString;
Connection._currentState = new ConnectedState(Connection);
Console.WriteLine("Connection opened");
}
public override void Close()
{
Console.WriteLine("Already disconnected");
}
public override void ExecuteQuery(string query)
{
throw new InvalidOperationException("Not connected");
}
}
private class ConnectedState : State
{
public ConnectedState(Connection connection) : base(connection) { }
public override void Open(string connectionString)
{
Console.WriteLine("Already connected");
}
public override void Close()
{
Connection._currentState = new DisconnectedState(Connection);
Console.WriteLine("Connection closed");
}
public override void ExecuteQuery(string query)
{
Console.WriteLine($"Executing: {query}");
}
}
}
Each state class manipulates the Connection's private fields directly. This tight coupling is acceptable because these states exist solely to implement the connection's behavior. The pattern clearly separates state-specific logic while keeping everything together in one file.
Exposing Public Nested Types
Sometimes you want users to know about nested types. Public nested types let you organize related types hierarchically while making their relationship explicit. The outer type acts as a namespace, grouping related types together.
You'll commonly see this pattern in configuration classes or when a class exposes options that only make sense in its context. The nested type's name becomes OuterClass.InnerClass, which clearly communicates that the inner type relates to the outer one.
public class HttpClient
{
private readonly Options _options;
public HttpClient(Options options)
{
_options = options;
}
public async Task<string> GetAsync(string url)
{
Console.WriteLine($"Timeout: {_options.Timeout}");
Console.WriteLine($"Retry: {_options.RetryCount} times");
await Task.Delay(100);
return "Response data";
}
// Public nested type for configuration
public class Options
{
public int Timeout { get; set; } = 30;
public int RetryCount { get; set; } = 3;
public bool AllowRedirects { get; set; } = true;
public void Validate()
{
if (Timeout <= 0)
throw new ArgumentException("Timeout must be positive");
if (RetryCount < 0)
throw new ArgumentException("RetryCount cannot be negative");
}
}
}
// Usage shows the relationship clearly
var options = new HttpClient.Options
{
Timeout = 60,
RetryCount = 5
};
options.Validate();
var client = new HttpClient(options);
The HttpClient.Options syntax immediately tells you these options belong to HttpClient. This is clearer than a separate HttpClientOptions class at namespace level, which could theoretically work with other HTTP client implementations. The nested type makes the ownership explicit.
Try It Yourself
This example shows a builder pattern using nested types. The builder is nested because it exists only to construct the outer class, and making this relationship explicit through nesting improves code clarity.
Steps:
- dotnet new console -n NestedTypesDemo
- cd NestedTypesDemo
- Replace Program.cs with the code below
- Create NestedTypesDemo.csproj as shown
- dotnet run
using System;
public class Query
{
private readonly string _table;
private readonly string[] _columns;
private readonly string? _whereClause;
private readonly int _limit;
private Query(string table, string[] columns, string? whereClause, int limit)
{
_table = table;
_columns = columns;
_whereClause = whereClause;
_limit = limit;
}
public string ToSql()
{
var sql = $"SELECT {string.Join(", ", _columns)} FROM {_table}";
if (_whereClause != null)
sql += $" WHERE {_whereClause}";
if (_limit > 0)
sql += $" LIMIT {_limit}";
return sql;
}
public class Builder
{
private string _table = "";
private string[] _columns = Array.Empty<string>();
private string? _whereClause;
private int _limit;
public Builder From(string table)
{
_table = table;
return this;
}
public Builder Select(params string[] columns)
{
_columns = columns;
return this;
}
public Builder Where(string condition)
{
_whereClause = condition;
return this;
}
public Builder Limit(int count)
{
_limit = count;
return this;
}
public Query Build()
{
if (string.IsNullOrEmpty(_table))
throw new InvalidOperationException("Table name required");
if (_columns.Length == 0)
throw new InvalidOperationException("At least one column required");
return new Query(_table, _columns, _whereClause, _limit);
}
}
}
// Usage
var query = new Query.Builder()
.From("Users")
.Select("Id", "Name", "Email")
.Where("Active = 1")
.Limit(10)
.Build();
Console.WriteLine(query.ToSql());
The builder pattern benefits from nesting because Query.Builder immediately communicates that this builder constructs Query objects. The private constructor ensures the only way to create a Query is through the builder, enforcing validation at build time.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Output:
SELECT Id, Name, Email FROM Users WHERE Active = 1 LIMIT 10
The fluent interface makes query building readable while the nested type structure makes it clear the builder belongs to Query. This organizational choice helps developers discover the builder through IntelliSense when they have a Query instance.
When Not to Use Nested Types
Nested types create tight coupling that's sometimes exactly what you need, but often it's the wrong choice. If your "nested" type might be useful in other contexts, make it a separate class at namespace level instead. Testing becomes harder with nested types because you can't easily mock or stub them, unlike interface-based dependencies that you can swap out.
Avoid nesting types more than one level deep. Deeply nested types like Outer.Middle.Inner create confusing hierarchies that are hard to navigate. If you're tempted to nest multiple levels, you probably need better namespace organization or separate files instead. Two levels is the practical maximum before cognitive load becomes a problem.
Skip nested types when you need multiple classes to share the "inner" type. Nested types belong to one container, so if two different classes need to use the same helper class, it shouldn't be nested in either. Create a separate class that both can depend on, or use an interface to decouple the implementations. Nesting in one and exposing it publicly just to share creates awkward dependencies.
Public nested types should be rare. They make sense for builders, options classes, and types that truly belong to their container. If you're making a nested type public just to expose it for testing, that's a design smell. Consider whether the type should actually be separate, or whether your tests are verifying implementation details rather than behavior. Most nested types should remain private implementation details that callers never see.