Manikandan — Manikandan
Updated on 4 min read Manikandan Design Patterns

Visitor Design Pattern in .NET Core API

A practical Visitor pattern guide in C# with classic and acyclic visitor styles for stable object structures.

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

PartPurposeExample in this article
ElementAccepts a visitorInvoiceLine, InvoiceSummary
VisitorDefines operationsIInvoiceVisitor
Concrete visitorImplements a specific operationInvoiceTotalVisitor
Object structureCollection of elementsinvoice 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

VariationCouplingFlexibilityBest use case
Classic VisitorTighterHigher dispatch claritySmall to medium stable hierarchies
Acyclic VisitorLooserHigher modularityLarger 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 --> IInvoiceElement

A 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.

Share:
Back to Blog

Related Posts

View All Posts »