When to Use Visitor Pattern in C#: Decision Guide with Examples
You understand the visitor pattern conceptually. You can describe double dispatch and draw the class diagram. But the harder question is: when to use the visitor pattern in C# in real production code? Choosing the right design pattern for the right problem is what separates clean architecture from unnecessary complexity -- and this guide will help you make that distinction with clarity.
The visitor pattern is one of the most misunderstood behavioral patterns. It's powerful when the conditions are right, and it's a source of confusion when applied to problems it wasn't designed to solve. In this article, we'll walk through the decision criteria that signal a good fit for the visitor pattern in C#, explore practical scenarios with code examples, identify the situations where you should skip the visitor pattern entirely, and give you a decision checklist you can reference in your designs.
Decision Criteria: When Does the Visitor Pattern Fit?
Before jumping into scenarios, let's nail down the conditions where the visitor pattern earns its place. Not every "do something to a collection of objects" problem calls for a visitor pattern implementation -- but when several of these criteria line up, you're likely looking at a strong candidate.
You need to perform multiple unrelated operations on an object structure. This is the core motivation. If you have a tree, graph, or collection of objects and you keep adding new operations that cut across all element types, the visitor pattern lets you define each operation in its own class. Instead of scattering operation logic across every element, each visitor encapsulates a single algorithm. This keeps your element classes focused on what they represent rather than what gets done to them.
The set of element types is stable, but operations change frequently. The visitor pattern thrives when you rarely add new element types but regularly add new operations. If your class hierarchy is locked down -- think AST nodes in a compiler or shapes in a graphics engine -- every new operation becomes a new visitor class. No existing code changes. That's the sweet spot.
You need double dispatch. In C#, method calls are dispatched based on the runtime type of the receiver. But what if you need dispatch based on the runtime types of two objects -- the element and the operation? The visitor pattern achieves this through its Accept/Visit handshake. The element calls back into the visitor with its concrete type, giving you type-safe behavior without casting or type checks.
You want to keep algorithms separate from the data structures they operate on. When your element classes would otherwise accumulate methods for rendering, serializing, validating, and reporting, the visitor pattern pulls each concern into its own class. This aligns with the single responsibility principle and makes it straightforward to test each operation independently.
You're working with heterogeneous collections. If your collection contains elements of different concrete types and each type needs different treatment during an operation, the visitor pattern provides a clean alternative to cascading if-else or switch statements with type checks. Each visitor method handles exactly one element type.
When you see two or three of these criteria converging in the same problem, the visitor pattern is worth serious consideration. Understanding when to use visitor pattern in C# starts with recognizing these signals. Let's look at what that looks like in code.
Scenario: AST Processing in a Rule Engine
One of the classic applications of the visitor pattern in C# is walking abstract syntax trees. If you're building a rule engine, expression evaluator, or any system that parses structured input into a tree, you'll need to perform multiple passes over that tree -- evaluation, pretty-printing, optimization, and validation are common examples.
Here's a typical AST structure with an Accept method on each node:
public interface IExpression
{
T Accept<T>(IExpressionVisitor<T> visitor);
}
public sealed class NumberExpression : IExpression
{
public double Value { get; }
public NumberExpression(double value)
{
Value = value;
}
public T Accept<T>(IExpressionVisitor<T> visitor)
{
return visitor.VisitNumber(this);
}
}
public sealed class BinaryExpression : IExpression
{
public IExpression Left { get; }
public IExpression Right { get; }
public string Operator { get; }
public BinaryExpression(
IExpression left,
string op,
IExpression right)
{
Left = left;
Operator = op;
Right = right;
}
public T Accept<T>(IExpressionVisitor<T> visitor)
{
return visitor.VisitBinary(this);
}
}
Now define the visitor interface and an evaluation visitor:
public interface IExpressionVisitor<T>
{
T VisitNumber(NumberExpression expression);
T VisitBinary(BinaryExpression expression);
}
public sealed class EvaluationVisitor
: IExpressionVisitor<double>
{
public double VisitNumber(NumberExpression expression)
{
return expression.Value;
}
public double VisitBinary(BinaryExpression expression)
{
double left = expression.Left.Accept(this);
double right = expression.Right.Accept(this);
return expression.Operator switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
_ => throw new InvalidOperationException(
$"Unknown operator: {expression.Operator}")
};
}
}
Adding a new operation -- say, pretty-printing -- means writing a new visitor class. No changes to the expression types:
public sealed class PrintVisitor
: IExpressionVisitor<string>
{
public string VisitNumber(NumberExpression expression)
{
return expression.Value.ToString();
}
public string VisitBinary(BinaryExpression expression)
{
string left = expression.Left.Accept(this);
string right = expression.Right.Accept(this);
return $"({left} {expression.Operator} {right})";
}
}
This is the visitor pattern doing exactly what it was designed for. Each operation is isolated in its own class, and the expression nodes remain untouched. This scenario illustrates when to use visitor pattern in C# most clearly -- a stable set of node types with a growing list of operations. If you're building something that resembles the interpreter pattern, the visitor pattern is a natural complement for separating interpretation logic from grammar structure.
Scenario: Reporting Across a Composite Structure
Another scenario where the visitor pattern fits well is generating reports from a composite structure. Consider a document model where you have paragraphs, images, tables, and sections that contain other elements. You need to generate HTML output, extract plain text, count words, and calculate reading time -- all different operations over the same tree.
Without the visitor pattern, you'd be tempted to add methods like ToHtml(), ToPlainText(), and GetWordCount() directly to each element. That works at first, but every new report format means modifying every element class. The visitor pattern in C# flips this -- each report becomes its own visitor:
public interface IDocumentElement
{
void Accept(IDocumentVisitor visitor);
}
public sealed class Paragraph : IDocumentElement
{
public string Text { get; }
public Paragraph(string text)
{
Text = text;
}
public void Accept(IDocumentVisitor visitor)
{
visitor.VisitParagraph(this);
}
}
public sealed class Image : IDocumentElement
{
public string Url { get; }
public string AltText { get; }
public Image(string url, string altText)
{
Url = url;
AltText = altText;
}
public void Accept(IDocumentVisitor visitor)
{
visitor.VisitImage(this);
}
}
Now the HTML export visitor handles each element differently:
public interface IDocumentVisitor
{
void VisitParagraph(Paragraph paragraph);
void VisitImage(Image image);
}
public sealed class HtmlExportVisitor : IDocumentVisitor
{
private readonly StringBuilder _builder = new();
public void VisitParagraph(Paragraph paragraph)
{
_builder.AppendLine(
$"<p>{paragraph.Text}</p>");
}
public void VisitImage(Image image)
{
_builder.AppendLine(
$"<img src="{image.Url}" " +
$"alt="{image.AltText}" />");
}
public string GetResult() => _builder.ToString();
}
Adding a word count visitor or a plain text extractor is a new class each time. The document element types never change. If your composite tree is stable and you keep layering on new operations -- export formats, analytics, transformations -- the visitor pattern is the right tool to keep that complexity manageable. This is another textbook example of when to use visitor pattern in C# effectively.
Before and After: Refactoring to the Visitor Pattern
Let's look at what code looks like before and after introducing the visitor pattern in C# so you can see the improvement clearly. Consider a shipping cost calculator that handles different package types:
Before -- Type Checks Scattered Across Operations
public decimal CalculateShippingCost(IPackage package)
{
if (package is StandardPackage std)
{
return std.Weight * 2.5m;
}
else if (package is FragilePackage fragile)
{
return fragile.Weight * 4.0m + 5.0m;
}
else if (package is OversizedPackage oversized)
{
return oversized.Weight * 3.0m
+ oversized.ExtraDimensionFee;
}
throw new ArgumentException("Unknown package type");
}
public string GenerateLabel(IPackage package)
{
if (package is StandardPackage std)
{
return $"STD-{std.TrackingId}";
}
else if (package is FragilePackage fragile)
{
return $"FRAG-{fragile.TrackingId}-HANDLE WITH CARE";
}
else if (package is OversizedPackage oversized)
{
return $"OVER-{oversized.TrackingId}-SPECIAL HANDLING";
}
throw new ArgumentException("Unknown package type");
}
Every new operation means another method full of type checks. Every new package type means updating every method. This gets unwieldy fast.
After -- Visitor Pattern Applied
public interface IPackage
{
T Accept<T>(IPackageVisitor<T> visitor);
}
public sealed class StandardPackage : IPackage
{
public decimal Weight { get; init; }
public string TrackingId { get; init; } = string.Empty;
public T Accept<T>(IPackageVisitor<T> visitor)
=> visitor.VisitStandard(this);
}
public sealed class FragilePackage : IPackage
{
public decimal Weight { get; init; }
public string TrackingId { get; init; } = string.Empty;
public T Accept<T>(IPackageVisitor<T> visitor)
=> visitor.VisitFragile(this);
}
public sealed class OversizedPackage : IPackage
{
public decimal Weight { get; init; }
public string TrackingId { get; init; } = string.Empty;
public decimal ExtraDimensionFee { get; init; }
public T Accept<T>(IPackageVisitor<T> visitor)
=> visitor.VisitOversized(this);
}
Now each operation lives in its own visitor:
public interface IPackageVisitor<T>
{
T VisitStandard(StandardPackage package);
T VisitFragile(FragilePackage package);
T VisitOversized(OversizedPackage package);
}
public sealed class ShippingCostVisitor
: IPackageVisitor<decimal>
{
public decimal VisitStandard(StandardPackage package)
=> package.Weight * 2.5m;
public decimal VisitFragile(FragilePackage package)
=> package.Weight * 4.0m + 5.0m;
public decimal VisitOversized(OversizedPackage package)
=> package.Weight * 3.0m + package.ExtraDimensionFee;
}
public sealed class LabelGeneratorVisitor
: IPackageVisitor<string>
{
public string VisitStandard(StandardPackage package)
=> $"STD-{package.TrackingId}";
public string VisitFragile(FragilePackage package)
=> $"FRAG-{package.TrackingId}-HANDLE WITH CARE";
public string VisitOversized(OversizedPackage package)
=> $"OVER-{package.TrackingId}-SPECIAL HANDLING";
}
Usage becomes clean and type-safe:
IPackage package = new FragilePackage
{
Weight = 3.5m,
TrackingId = "PKG-2024-0042"
};
var costVisitor = new ShippingCostVisitor();
decimal cost = package.Accept(costVisitor);
var labelVisitor = new LabelGeneratorVisitor();
string label = package.Accept(labelVisitor);
No type checks. No casting. Adding a new operation is a new visitor class. The visitor pattern eliminates the scattered type-checking that makes code brittle and hard to maintain. You can even register these visitors through dependency injection if you need runtime flexibility in which operations are applied.
When NOT to Use the Visitor Pattern
Knowing when to use visitor pattern in C# also means knowing when to skip it. The visitor pattern introduces structural complexity, and that cost is only justified when the problem truly demands it.
If your element types change frequently, the visitor pattern in C# fights you. Every new element type means updating every visitor interface and every visitor implementation. If you're adding new shapes, node types, or document elements regularly, the visitor pattern creates a maintenance burden. In that scenario, putting behavior directly on the elements -- or using the strategy pattern to swap algorithms -- is a better fit.
If you only have one operation, the visitor pattern is overkill. The pattern's value comes from separating multiple operations from a stable set of types. If you only need one operation, adding an Accept method and a visitor interface is unnecessary indirection. Just put the logic where it belongs and move on.
If your collection is homogeneous, you don't need a visitor. When every element in your collection is the same concrete type, a simple foreach loop or LINQ expression handles the work. The visitor pattern solves the problem of dispatching to the right method based on element type -- a problem that doesn't exist when all elements are the same type.
If the operation doesn't need access to the element's internal state, the visitor might not add value. The visitor pattern is designed for situations where the operation needs to interact with type-specific properties. If you're performing a generic operation that works through a common interface -- like calling ToString() on everything -- a visitor adds ceremony without benefit.
If the indirection confuses your team, reconsider. The double dispatch mechanism in the visitor pattern trips up developers who haven't seen it before. If your team is small, the codebase is young, and the problem is straightforward, the readability cost might outweigh the flexibility gain. Sometimes a clean switch statement is the more pragmatic choice. A facade that simplifies the interface might serve you better if the underlying complexity doesn't warrant the full visitor infrastructure.
Decision Checklist: Should You Use the Visitor Pattern?
Walk through these questions when you're evaluating whether the visitor pattern fits your problem. This checklist helps you determine when to use visitor pattern in C# with confidence:
1. Do you have a collection of objects with multiple concrete types that need different treatment? If yes -- the visitor pattern gives you type-safe dispatch without manual type checks.
2. Are you adding new operations to the object structure more often than new element types? If yes -- this is the visitor pattern's sweet spot. New operations are new classes with no modification to existing code.
3. Do your element types form a stable, well-defined hierarchy? If yes -- the visitor interface maps cleanly to your element set. If the hierarchy is volatile, each new element forces changes across all visitors.
4. Do you need the operation to access type-specific properties -- not just a common interface? If yes -- double dispatch through the visitor gives you compile-time type safety for accessing those properties.
5. Are multiple unrelated operations accumulating on your element classes? If yes -- the visitor pattern pulls those operations out into separate classes, keeping elements focused and operations cohesive.
6. Could you achieve the same result with a simple polymorphic method on the element interface? If yes -- you probably don't need the visitor pattern. An abstract method on the base type is simpler when you have one or two operations.
7. Is the chain of responsibility or observer pattern a better fit? If your problem is about routing a request to a handler or broadcasting events to subscribers, those patterns address fundamentally different concerns than the visitor pattern.
If you answered "yes" to questions 1 through 5 and "no" to question 6, the visitor pattern in C# is a strong candidate. The more of those conditions that align, the more confident you can be in the decision.
Wrapping Up
The visitor pattern in C# is a precise tool for a specific set of problems -- separating algorithms from the object structures they operate on when you have a stable type hierarchy and a growing list of operations. It delivers clean double dispatch, eliminates scattered type checks, and keeps each operation encapsulated in its own class. But it comes with structural overhead that only pays off when the criteria align. The decision criteria, scenarios, and checklist in this article should give you a practical framework for evaluating when to use the visitor pattern in C# the next time you're weighing your design options. Start with the simplest approach that works, and reach for the visitor pattern when the problem genuinely demands it.
Frequently Asked Questions
What is double dispatch and why does the visitor pattern need it?
Double dispatch is a technique where the method that gets called depends on the runtime types of two objects -- not just one. In C#, regular method calls dispatch based on the receiver's type. The visitor pattern uses the Accept/Visit handshake to achieve dispatch based on both the element type and the visitor type. The element's Accept method calls the visitor with its concrete type, which lets the visitor resolve to the correct overloaded method. Without double dispatch, you'd be stuck writing manual type checks or casts.
Can I use the visitor pattern with sealed class hierarchies in C#?
Yes, and sealed hierarchies are actually an excellent fit. When your element types are sealed, you know the complete set of types at compile time. This means your visitor interface can cover every case, and the compiler helps you catch missing implementations. Sealed types also prevent external code from adding new element types that your visitors wouldn't handle, which is exactly the stability the visitor pattern requires.
How does the visitor pattern compare to pattern matching in C#?
C# pattern matching with switch expressions can handle type-based dispatch without the visitor infrastructure. For simple cases with one or two operations, pattern matching is often cleaner. The visitor pattern becomes preferable when you have many operations that all need type-based dispatch, because each visitor encapsulates one operation in a dedicated class. Pattern matching scatters that logic across multiple switch expressions. The visitor pattern also gives you a compiler error if you forget to handle a type -- pattern matching only does that with exhaustive switches.
Does the visitor pattern violate the open/closed principle?
It depends on the axis of change. The visitor pattern is open for extension along the operation axis -- you can add new visitors without modifying existing code. But it's closed along the element axis -- adding a new element type forces changes to the visitor interface and every visitor implementation. This tradeoff is fundamental to the pattern. If your element types change more often than your operations, the visitor pattern violates the open/closed principle for your use case, and you should consider alternatives like the strategy pattern.
When should I use a generic visitor interface versus a non-generic one?
Use a generic visitor interface like IVisitor<T> when your operations return a value -- evaluation, transformation, or serialization. Use a non-generic IVisitor with void Visit methods when the operation produces side effects, like rendering output or collecting statistics into an external data structure. The generic approach is more flexible and avoids the need for visitors to maintain mutable state, but both are valid depending on whether your operation naturally produces a return value.
Can I combine the visitor pattern with dependency injection?
Absolutely. You can register visitor implementations in your DI container and resolve them at runtime. This is particularly useful when visitors have dependencies of their own -- like a serialization visitor that needs an IOutputWriter or a validation visitor that needs access to configuration. You register each visitor against its interface and inject the one you need, keeping the calling code decoupled from concrete visitor types.
Is the visitor pattern still relevant with modern C# features?
Yes, though modern C# gives you alternatives for simpler cases. Pattern matching, switch expressions, and discriminated unions (if you use a library or wait for language support) handle basic type dispatch elegantly. The visitor pattern remains relevant when you need structured separation of multiple complex operations from a stable object hierarchy. It provides compile-time safety, clear organization of operation logic, and a well-understood contract between elements and operations that ad-hoc pattern matching doesn't enforce.

