How to Implement Composite Pattern in C#: Step-by-Step Guide
When you need to represent part-whole hierarchies where individual objects and groups of objects should be treated the same way, the composite pattern is the structural design pattern you want. If you're looking to implement composite pattern in C#, this guide will walk you through every step -- from defining your component interface to building recursive tree operations and wiring everything into a dependency injection container. By the end, you'll have a working file system model that demonstrates the pattern's full power and a clear understanding of how to apply it in your own projects.
We'll build progressively -- starting with a simple interface, then adding leaf nodes, composite containers, and finally layering on recursive operations. Each step includes complete C# code you can compile and adapt.
Prerequisites
Before diving into the implementation, make sure you're comfortable with a few foundational C# concepts. You don't need to be an expert, but a working knowledge of these topics will help everything click faster.
You should understand interfaces and how they define contracts that multiple classes can implement. The composite pattern relies on a shared interface between leaf nodes and composite containers, so understanding how polymorphism works through interfaces is essential. You'll also want to be familiar with inheritance and how classes can share behavior through composition rather than deep inheritance chains.
Finally, you'll need a basic grasp of generic collections like List<T> since composites store their children in collections. If you've worked with List<T>, foreach loops, and LINQ basics, you're in good shape.
Step 1: Define the Component Interface
The component interface is the foundation of the composite pattern. It declares the operations that both individual elements (leaves) and containers (composites) must support. This shared contract lets client code treat single objects and groups uniformly -- and that uniformity is the entire point of implementing the composite pattern in C#.
For our file system example, we'll define an IFileSystemComponent interface with operations for getting the name, calculating size, and displaying the structure:
using System;
using System.Collections.Generic;
public interface IFileSystemComponent
{
string Name { get; }
long GetSize();
void Display(int indentLevel = 0);
}
Notice that the interface is deliberately simple. Every component -- whether it's a single file or a directory containing hundreds of nested items -- exposes the same three members. The Display method takes an optional indentLevel parameter so we can visualize the tree structure later. The GetSize method will behave differently depending on whether the component is a leaf or a composite, but the calling code doesn't need to know which type it's dealing with.
This is where the composite pattern shines. By programming against the IFileSystemComponent interface, you can write algorithms that operate on the tree without caring about its internal structure.
Step 2: Create Leaf Classes
Leaf classes represent the terminal nodes in your tree -- objects that don't contain other components. In our file system analogy, a leaf is a file. It has a name and a size, and it implements the component interface directly with straightforward logic.
Here's the FileLeaf class:
using System;
public class FileLeaf : IFileSystemComponent
{
public string Name { get; }
public long SizeInBytes { get; }
public FileLeaf(string name, long sizeInBytes)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
if (sizeInBytes < 0)
{
throw new ArgumentOutOfRangeException(
nameof(sizeInBytes),
"File size cannot be negative.");
}
SizeInBytes = sizeInBytes;
}
public long GetSize()
{
return SizeInBytes;
}
public void Display(int indentLevel = 0)
{
string indent = new string(' ', indentLevel * 2);
Console.WriteLine($"{indent}- {Name} ({SizeInBytes} bytes)");
}
}
A few things to notice here. The constructor validates its inputs -- null names and negative sizes are rejected immediately. This is important because when you implement the composite pattern in C# for production systems, defensive coding at the leaf level prevents corrupted data from propagating through the tree.
The Display method uses indentation to show where the file sits in the hierarchy. You can create as many leaf types as your domain requires -- for instance, a SymbolicLinkLeaf that implements the same interface but with slightly different behavior.
Step 3: Build the Composite Class
The composite class is the heart of the pattern. It implements the same component interface as the leaf but also manages a collection of child components. This is what creates the tree structure -- composites can contain both leaves and other composites, enabling arbitrary nesting depth.
Here's the DirectoryComposite class:
using System;
using System.Collections.Generic;
using System.Linq;
public class DirectoryComposite : IFileSystemComponent
{
private readonly List<IFileSystemComponent> _children = new();
public string Name { get; }
public DirectoryComposite(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
public void Add(IFileSystemComponent component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (ReferenceEquals(component, this))
{
throw new InvalidOperationException(
"A directory cannot contain itself.");
}
_children.Add(component);
}
public void Remove(IFileSystemComponent component)
{
_children.Remove(component);
}
public IFileSystemComponent? GetChild(int index)
{
if (index < 0 || index >= _children.Count)
{
return null;
}
return _children[index];
}
public IReadOnlyList<IFileSystemComponent> GetChildren()
{
return _children.AsReadOnly();
}
public long GetSize()
{
return _children.Sum(child => child.GetSize());
}
public void Display(int indentLevel = 0)
{
string indent = new string(' ', indentLevel * 2);
Console.WriteLine($"{indent}+ {Name}/");
foreach (IFileSystemComponent child in _children)
{
child.Display(indentLevel + 1);
}
}
}
Let's break down the critical design decisions. The _children list is private, and the class exposes Add, Remove, GetChild, and GetChildren methods to manage it. The GetChildren method returns an IReadOnlyList<T> so external code can iterate the children without modifying the internal collection. This protects the composite's integrity.
The Add method includes a self-reference check with ReferenceEquals(component, this). Without this guard, you could accidentally create a directory that contains itself, which would cause infinite recursion when calling GetSize or Display. We'll talk more about this pitfall later, but it's worth calling out now because it's easy to miss when you first implement the composite pattern in C#.
The GetSize method is where the recursive magic happens. It uses LINQ's Sum to add up the sizes of all children. If a child is a FileLeaf, it returns its own size directly. If a child is another DirectoryComposite, it recursively sums its own children. The entire tree gets traversed with a single method call, and the calling code doesn't need to know anything about the structure.
Step 4: Wire It Together
Now that we have our component interface, leaf class, and composite class, let's build a realistic tree and see everything working together. This is where the pattern starts to feel practical -- you'll see how client code can build complex structures and operate on them uniformly.
using System;
// Build the tree structure
DirectoryComposite root = new("project");
DirectoryComposite srcDir = new("src");
srcDir.Add(new FileLeaf("Program.cs", 2048));
srcDir.Add(new FileLeaf("Startup.cs", 1536));
DirectoryComposite modelsDir = new("Models");
modelsDir.Add(new FileLeaf("User.cs", 512));
modelsDir.Add(new FileLeaf("Order.cs", 768));
srcDir.Add(modelsDir);
DirectoryComposite testsDir = new("tests");
testsDir.Add(new FileLeaf("UserTests.cs", 1024));
testsDir.Add(new FileLeaf("OrderTests.cs", 896));
root.Add(srcDir);
root.Add(testsDir);
root.Add(new FileLeaf("README.md", 256));
// Operate on the tree uniformly
root.Display();
Console.WriteLine();
Console.WriteLine($"Total project size: {root.GetSize()} bytes");
Running this code produces output like:
+ project/
+ src/
- Program.cs (2048 bytes)
- Startup.cs (1536 bytes)
+ Models/
- User.cs (512 bytes)
- Order.cs (768 bytes)
+ tests/
- UserTests.cs (1024 bytes)
- OrderTests.cs (896 bytes)
- README.md (256 bytes)
Total project size: 7040 bytes
The beauty of this approach is that root.GetSize() and root.Display() work identically whether root contains 3 items or 3000. You can pass any IFileSystemComponent to a method that expects the interface, and it behaves correctly regardless of whether it's a leaf or a deeply nested composite. This is the core value when you implement composite pattern in C# -- uniform treatment of individual and composed objects.
Step 5: Add Recursive Operations
Once you have the basic tree structure working, you'll likely want operations that go beyond simple size calculation. The composite pattern makes it straightforward to add recursive behaviors that traverse the entire tree. Let's add search functionality and a more detailed print operation.
First, here's a utility class that performs operations across any IFileSystemComponent tree:
using System;
using System.Collections.Generic;
public static class FileSystemOperations
{
public static IFileSystemComponent? Search(
IFileSystemComponent component,
string name)
{
if (component.Name.Equals(
name,
StringComparison.OrdinalIgnoreCase))
{
return component;
}
if (component is DirectoryComposite directory)
{
foreach (IFileSystemComponent child
in directory.GetChildren())
{
IFileSystemComponent? result =
Search(child, name);
if (result != null)
{
return result;
}
}
}
return null;
}
public static int CountFiles(
IFileSystemComponent component)
{
if (component is FileLeaf)
{
return 1;
}
if (component is DirectoryComposite directory)
{
int count = 0;
foreach (IFileSystemComponent child
in directory.GetChildren())
{
count += CountFiles(child);
}
return count;
}
return 0;
}
public static List<string> GetAllFilePaths(
IFileSystemComponent component,
string currentPath = "")
{
List<string> paths = new();
string fullPath = string.IsNullOrEmpty(currentPath)
? component.Name
: $"{currentPath}/{component.Name}";
if (component is FileLeaf)
{
paths.Add(fullPath);
}
else if (component is DirectoryComposite directory)
{
foreach (IFileSystemComponent child
in directory.GetChildren())
{
paths.AddRange(
GetAllFilePaths(child, fullPath));
}
}
return paths;
}
}
Here's how you'd use these operations with the tree we built earlier:
using System;
// Search for a specific file
IFileSystemComponent? found =
FileSystemOperations.Search(root, "User.cs");
if (found != null)
{
Console.WriteLine(
$"Found: {found.Name} ({found.GetSize()} bytes)");
}
// Count all files in the tree
int fileCount = FileSystemOperations.CountFiles(root);
Console.WriteLine($"Total files: {fileCount}");
// Get all file paths
List<string> allPaths =
FileSystemOperations.GetAllFilePaths(root);
foreach (string path in allPaths)
{
Console.WriteLine(path);
}
Each of these methods follows the same recursive pattern. Check if the current node is a leaf and handle it directly. If it's a composite, iterate through the children and recurse. This pattern repeats across virtually every operation you'll write against a composite tree.
The Search method demonstrates early termination -- once it finds a match, it returns immediately. The GetAllFilePaths method builds path strings as it descends, giving you fully qualified paths relative to the root.
Step 6: Integration with Dependency Injection
In a real .NET application, you'll want to register your composite pattern components with the dependency injection container. This lets you resolve tree structures from the DI container and keeps your application loosely coupled through inversion of control.
Here's how you might register composite pattern components with IServiceCollection:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
public static class CompositeServiceExtensions
{
public static IServiceCollection AddFileSystem(
this IServiceCollection services)
{
// Register the leaf factory
services.AddTransient<Func<string, long, FileLeaf>>(
_ => (name, size) => new FileLeaf(name, size));
// Register the composite factory
services.AddTransient<Func<string, DirectoryComposite>>(
_ => name => new DirectoryComposite(name));
// Register operations as a singleton
// since they're stateless
services.AddSingleton<IFileSystemOperations>(
_ => new FileSystemOperationsService());
return services;
}
}
The factory registration pattern works well for composite structures because trees are typically built dynamically rather than resolved as a single graph. You inject the factories where you need them and build your trees at runtime.
For scenarios where you want the operations themselves to be injectable and testable, extract an interface from the static operations class:
using System.Collections.Generic;
public interface IFileSystemOperations
{
IFileSystemComponent? Search(
IFileSystemComponent root,
string name);
int CountFiles(IFileSystemComponent root);
List<string> GetAllFilePaths(
IFileSystemComponent root);
}
This approach combines the composite pattern with clean dependency injection practices. Your controllers or services receive an IFileSystemOperations instance and work against the IFileSystemComponent interface, which means they're testable, mockable, and decoupled from the concrete tree implementation.
Common Implementation Pitfalls
When you implement composite pattern in C#, there are several mistakes that catch developers off guard. Knowing about them upfront saves you debugging time later.
Forgetting null checks in Add/Remove is one of the most frequent issues. If your Add method doesn't validate its input, a null child silently enters the collection and causes a NullReferenceException the next time you call GetSize() or Display(). Always guard against null in methods that modify the children collection. The ArgumentNullException thrown in our Add method is there for exactly this reason.
Creating circular references is a more subtle problem. If Directory A contains Directory B, and Directory B gets a reference back to Directory A, any recursive operation will loop until you hit a StackOverflowException. Our self-reference check catches the simplest case, but it won't catch indirect cycles. For production code, consider implementing cycle detection that walks up the parent chain before allowing an add operation.
Exposing the internal children list directly is a design smell that leads to hard-to-trace bugs. If you return _children as a List<T> instead of wrapping it with AsReadOnly(), external code can add, remove, or reorder children without going through your validation logic. Always expose children through an IEnumerable<T> or IReadOnlyList<T> to maintain encapsulation.
Putting child-management methods on the component interface is a design decision that divides developers. Some implementations add Add() and Remove() to the component interface, which means leaf classes need to throw NotSupportedException. This violates the Interface Segregation Principle. Our implementation keeps child management on the composite class only, which is cleaner when you implement the composite pattern in C#.
Ignoring thread safety matters if your composite tree is accessed from multiple threads. The List<T> backing the children collection isn't thread-safe, so concurrent Add and GetSize calls can produce corrupt results. Consider using ConcurrentBag<T> or wrapping operations in locks if you need thread safety.
Exploring More Design Patterns
The composite pattern is just one of many structural patterns that help you organize complex C# applications. If you're building up your design pattern toolkit, you might find it useful to explore the big list of design patterns for a comprehensive overview of all the patterns available to you.
The composite pattern pairs especially well with other structural and behavioral patterns. The decorator pattern lets you add responsibilities to individual components without modifying the tree structure. The strategy pattern allows you to swap out the algorithms used during tree traversal. And if you're building extensible systems, plugin architecture in C# leverages many of the same composition principles that make the composite pattern effective.
Understanding how to implement the pipeline design pattern can also complement your composite work -- pipelines process data through a sequence of stages, and each stage could operate on composite tree structures to transform hierarchical data.
Frequently Asked Questions
When should I use the composite pattern instead of a simple list?
Use the composite pattern when your data has a hierarchical, tree-like structure where containers can hold both individual items and other containers. A simple List<T> works fine for flat collections, but it breaks down when you need nesting. If you find yourself writing recursive methods to traverse nested lists, that's a strong signal to implement the composite pattern in C# instead.
Can leaf nodes have children in the composite pattern?
No -- by definition, leaf nodes are terminal. They don't contain other components. If you need a node to sometimes hold children, model it as a composite with an empty children list rather than adding conditional logic to leaves.
How do I handle different types of operations across the tree?
You have a few strategies depending on how many operations you need. For a small number of operations, add them directly to the component interface (like GetSize and Display). For a larger or evolving set of operations, consider the Visitor pattern, which lets you define new operations without modifying the component classes. You can also use external utility classes with recursive methods, as we did with FileSystemOperations, which keeps the component interface slim while still supporting arbitrary traversal logic.
Is the composite pattern the same as composition in C#?
Not exactly. Composition in C# refers to the general practice of building objects by combining other objects -- favoring "has-a" relationships over "is-a" inheritance. The composite design pattern is a specific structural pattern that uses composition to represent part-whole hierarchies with a uniform interface. All composite pattern implementations use composition, but not all uses of composition in C# are composite pattern implementations. The pattern adds the specific constraint that both parts and wholes implement the same interface.
How do I prevent infinite recursion in composite trees?
The primary defense is preventing circular references at the point of insertion. Our Add method checks for direct self-references, but you should also guard against indirect cycles in production code. One approach is to maintain a parent reference on each component and walk up the ancestor chain when adding a child -- if the child-to-be appears as an ancestor, reject the operation. Another approach is to set a maximum depth limit on recursive operations as a safety net.
Can I serialize a composite tree to JSON?
Yes, and it works naturally because the tree structure maps well to JSON's hierarchical format. Using System.Text.Json, you can serialize composites by including a type discriminator property so the deserializer knows whether to create a leaf or a composite. You'll need a custom JsonConverter<IFileSystemComponent> that writes and reads the discriminator. Libraries like System.Text.Json with polymorphic serialization support (available in .NET 7+) make this even easier with the [JsonDerivedType] attribute on your component interface.
How does the composite pattern relate to the decorator pattern?
Both patterns rely on recursive composition and a shared interface, but they serve different purposes. The composite pattern organizes objects into tree structures to represent part-whole hierarchies. The decorator pattern wraps an object to add behavior without changing the original class. You can combine them effectively -- for example, wrapping a composite component with a decorator that adds caching to GetSize() calls. They complement each other well in larger systems where you need both hierarchical structure and extensible behavior.

