Visitor Design Pattern in .NET Core API
A practical Visitor pattern guide in C# with classic and acyclic visitor styles for stable object structures.

Intro
Sometimes you need to add operations to a stable object structure without changing the objects themselves. The Visitor pattern keeps those operations separate by moving them into visitor objects.
What Is the Visitor Pattern?
Visitor is a behavioral design pattern that lets you define a new operation over a set of objects without modifying the objects’ classes.
In .NET, it is useful for tree traversal, exporters, code generation, and rule-based inspection of object graphs.
Visitor Class Example
The example below models a small invoice model with a visitor that calculates totals.
public interface IInvoiceElement{ void Accept(IInvoiceVisitor visitor);}
public interface IInvoiceVisitor{ void Visit(InvoiceLine line); void Visit(InvoiceSummary summary);}
public sealed record InvoiceLine(string Name, decimal Amount) : IInvoiceElement{ public void Accept(IInvoiceVisitor visitor) => visitor.Visit(this);}
public sealed record InvoiceSummary(decimal TaxRate) : IInvoiceElement{ public void Accept(IInvoiceVisitor visitor) => visitor.Visit(this);}Why This Works
The element hierarchy stays stable while operations move into visitors. That means you can add new behavior without editing every element class.
When to Use It
Visitor is a strong fit when the object structure is stable but the operations over it change often.
- compilers and AST processing
- exporters for XML, JSON, or reports
- validation and inspection across object graphs
- document or invoice traversal
- code analysis tools
If the object model changes more often than the operations, visitor can become expensive.
Important Note for ASP.NET Core
In ASP.NET Core, visitors often appear in serialization helpers, validation passes, and tree-processing services. Keep the visitor interfaces small so dependency registration stays clear.
builder.Services.AddScoped<IInvoiceVisitor, InvoiceTotalVisitor>();That allows you to add new operations without changing the element types themselves.
Core Components of the Pattern
| Part | Purpose | Example in this article |
|---|---|---|
| Element | Accepts a visitor | InvoiceLine, InvoiceSummary |
| Visitor | Defines operations | IInvoiceVisitor |
| Concrete visitor | Implements a specific operation | InvoiceTotalVisitor |
| Object structure | Collection of elements | invoice model or AST |
Common Visitor Variations
You will commonly apply the Visitor pattern in these two forms:
Classic Visitor
The element exposes a single visitor contract with overloaded visit methods.
public interface IInvoiceVisitor{ void Visit(InvoiceLine line);}Acyclic Visitor
Each element depends only on the visitor interface it needs, which reduces dependency cycles.
public interface IInvoiceLineVisitor{ void Visit(InvoiceLine line);}How the Variations Differ
| Variation | Coupling | Flexibility | Best use case |
|---|---|---|---|
| Classic Visitor | Tighter | Higher dispatch clarity | Small to medium stable hierarchies |
| Acyclic Visitor | Looser | Higher modularity | Larger systems with evolving visitors |
SOLID Principles Behind It
SRP - Single Responsibility Principle
Elements keep their own data, while visitors own operations.
OCP - Open/Closed Principle
You can add a new operation by adding a new visitor.
DIP - Dependency Inversion Principle
High-level operations depend on abstractions, not concrete element internals.
Advantages and Disadvantages
Advantages
- makes new operations easy to add
- keeps element classes stable
- works well for traversals and exports
- separates data structure from behavior
Disadvantages (and Optional Alternatives)
- adding a new element type can require updating visitors Optional pattern: Composite (when the main need is traversal and aggregation).
- visitor interfaces can grow quickly across large hierarchies Optional pattern: Acyclic Visitor (when decoupling is more important).
- double-dispatch can be unfamiliar to teams Optional pattern: Strategy (when behavior can be selected without visiting a graph).
UML Diagram
classDiagram class Client class IInvoiceElement { <<interface>> +Accept(visitor) } class IInvoiceVisitor { <<interface>> +Visit(line) +Visit(summary) } class InvoiceLine class InvoiceSummary class InvoiceTotalVisitor
InvoiceLine ..|> IInvoiceElement InvoiceSummary ..|> IInvoiceElement InvoiceTotalVisitor ..|> IInvoiceVisitor Client --> IInvoiceVisitor Client --> IInvoiceElementA Quick Usage Example
IInvoiceElement[] elements ={ new InvoiceLine("Item A", 100m), new InvoiceSummary(0.18m)};
var visitor = new InvoiceTotalVisitor();foreach (var element in elements) element.Accept(visitor);The caller traverses a stable structure while the visitor owns the operation being applied.
How to Validate the Pattern
Use these checks to confirm your Visitor implementation is healthy:
- operations should be addable without changing every element class
- each element should expose a clear accept method
- visitors should stay focused on one operation
- tests should verify traversal and dispatch behavior
- the hierarchy should be stable enough to justify the pattern
Is there any other way to check our implementations
- yes, add visitor tests for every element type in the hierarchy
- add integration tests for export or analysis workflows that use the visitor
Summary
The Visitor pattern separates operations from the object structure they act on, which is useful when the structure is stable and the behavior changes often. In .NET APIs, it is especially effective for traversal-heavy code such as export, analysis, and transformation pipelines.




