Mastering Design Patterns in C# and ASP.NET Core
Part 4.11 – Visitor Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐⭐☆ (Advanced)
Prerequisites: C#, OOP, Interfaces, Polymorphism, Inheritance, SOLID Principles, Dependency Injection
Table of Contents
Introduction
What is the Visitor Design Pattern?
Why Do We Need the Visitor Pattern?
The Problem the Visitor Pattern Solves
Real-World Analogy
Visitor Pattern Structure
UML Class Diagram
Components of the Visitor Pattern
The
IVisitorInterfaceThe
Accept()MethodUnderstanding Double Dispatch
Complete C# Console Application
Document Processing Example
Shopping Cart Example
Tax Calculation Example
ASP.NET Core Implementation
Visitor with Dependency Injection
Real-World Enterprise Examples
Visitor vs Strategy
Visitor vs Composite
Visitor vs Interpreter
Visitor vs Decorator
Advantages
Disadvantages
Best Practices
Common Mistakes
Unit Testing
Interview Questions
When Should You Use Visitor?
When Should You Avoid Visitor?
Key Takeaways
Conclusion
Coming Up Next
1. Introduction
Imagine an application that contains many different types of objects:
Customer
Order
Invoice
Product
Employee
Department
Over time, we may need to perform many different operations on these objects:
Calculate Tax
Generate Report
Export to XML
Export to JSON
Validate
Audit
Calculate Discount
Generate Statistics
One approach is to keep adding methods to every domain class.
For example:
public class Product
{
public void CalculateTax()
{
}
public void GenerateReport()
{
}
public void ExportToXml()
{
}
public void ExportToJson()
{
}
}
This becomes difficult to maintain.
The classes gradually become responsible for too many unrelated operations.
The Visitor Design Pattern provides another approach.
Instead of putting every operation inside the domain classes, we can move operations into separate Visitor classes.
2. What is the Visitor Design Pattern?
The Visitor Design Pattern allows us to define new operations on a group of related object types without modifying the classes of those objects.
The basic idea is:
Object Structure
|
↓
Accept Visitor
|
↓
Visitor performs operation
For example:
Document
|
+---------+---------+
| | |
↓ ↓ ↓
PDF Word Excel
| | |
+---------+---------+
|
↓
Visitor
|
+---------+---------+
| | |
↓ ↓ ↓
Export Validate Audit
The object structure remains stable while new operations can be added through new visitors.
3. Why Do We Need the Visitor Pattern?
Consider a document system.
We have:
PdfDocument
WordDocument
ExcelDocument
Initially we need:
Export
Later the requirements change.
We also need:
Validate
Calculate Size
Generate Report
Audit
Compress
Without Visitor, we might repeatedly modify every document class.
For example:
public class PdfDocument
{
public void Export()
{
}
public void Validate()
{
}
public void CalculateSize()
{
}
public void Audit()
{
}
}
Then we do the same for:
WordDocument
ExcelDocument
PowerPointDocument
This creates a maintenance problem.
The Visitor Pattern moves these operations into separate classes.
4. The Problem the Visitor Pattern Solves
Suppose we have:
Element A
Element B
Element C
and need several operations:
Operation 1
Operation 2
Operation 3
Without Visitor:
Element A → Operation 1
Element A → Operation 2
Element A → Operation 3
Element B → Operation 1
Element B → Operation 2
Element B → Operation 3
Element C → Operation 1
Element C → Operation 2
Element C → Operation 3
The classes become large.
With Visitor:
Element A
Element B
Element C
|
↓
Accept(visitor)
|
↓
Visitor
|
+── Operation 1
+── Operation 2
+── Operation 3
Operations become separate objects.
5. Real-World Analogy
Imagine a hospital with different types of patients:
Child
Adult
Senior
Different specialists visit these patients:
Doctor
Insurance Agent
Nutritionist
Auditor
The patient types remain the same.
But different visitors perform different operations.
For example:
Doctor → Examine Patient
Insurance Agent → Calculate Insurance
Nutritionist → Create Diet Plan
Auditor → Review Records
The patient doesn't need to contain every possible operation.
The visitor performs the operation.
This is the basic idea behind the Visitor Pattern.
6. Visitor Pattern Structure
The pattern typically contains:
Visitor
Concrete Visitor
Element
Concrete Element
Object Structure
The relationship looks like:
+--------------------+
| Visitor |
+--------------------+
| Visit(ElementA) |
| Visit(ElementB) |
+---------+----------+
|
+---------+----------+
| |
↓ ↓
ConcreteVisitorA ConcreteVisitorB
+--------------------+
| Element |
+--------------------+
| Accept(visitor) |
+---------+----------+
|
+---------+----------+
| |
↓ ↓
ElementA ElementB
7. UML Class Diagram
+-------------------------+
| IVisitor |
+-------------------------+
| + Visit(Product) |
| + Visit(Service) |
| + Visit(Order) |
+------------+------------+
|
+------------+------------+
| |
↓ ↓
+------------------+ +------------------+
| TaxVisitor | | ReportVisitor |
+------------------+ +------------------+
| Visit(Product) | | Visit(Product) |
| Visit(Service) | | Visit(Service) |
| Visit(Order) | | Visit(Order) |
+------------------+ +------------------+
+-------------------------+
| IElement |
+-------------------------+
| + Accept(IVisitor) |
+------------+------------+
|
+------------+------------+
| |
↓ ↓
+-------------+ +-------------+
| Product | | Service |
+-------------+ +-------------+
| Accept() | | Accept() |
+-------------+ +-------------+
8. Components of the Visitor Pattern
8.1 Visitor
Defines operations for each element type.
public interface IVisitor
{
void Visit(Product product);
void Visit(Service service);
}
8.2 Concrete Visitor
Implements a specific operation.
For example:
TaxVisitor
ReportVisitor
AuditVisitor
ExportVisitor
8.3 Element
Defines:
void Accept(IVisitor visitor);
8.4 Concrete Element
Examples:
Product
Service
Order
Invoice
Each element implements Accept().
9. The IVisitor Interface
Let's create a simple example.
public interface IVisitor
{
void Visit(Product product);
void Visit(Service service);
}
This means the visitor knows how to process:
Product
Service
10. The IElement Interface
public interface IElement
{
void Accept(IVisitor visitor);
}
Each element accepts a visitor.
11. Product
public class Product : IElement
{
public string Name { get; set; }
public decimal Price { get; set; }
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Notice:
visitor.Visit(this);
The current object is passed to the visitor.
12. Service
public class Service : IElement
{
public string Name { get; set; }
public decimal Price { get; set; }
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Again:
visitor.Visit(this);
The visitor gets the concrete type.
13. Understanding Double Dispatch
This is one of the most important concepts in the Visitor Pattern.
Suppose we have:
element.Accept(visitor);
At first glance, it looks like one method call.
But two types determine the final behavior:
Element Type
+
Visitor Type
For example:
product.Accept(taxVisitor);
The Product class executes:
visitor.Visit(this);
Because this is a Product, the compiler selects:
Visit(Product product)
So the operation depends on both:
Product
+
TaxVisitor
This technique is known as Double Dispatch.
14. Complete C# Console Application
Let's create a complete example.
Step 1 – Visitor Interface
public interface IVisitor
{
void Visit(Product product);
void Visit(Service service);
}
Step 2 – Element Interface
public interface IElement
{
void Accept(IVisitor visitor);
}
Step 3 – Product
public class Product : IElement
{
public string Name { get; }
public decimal Price { get; }
public Product(
string name,
decimal price)
{
Name = name;
Price = price;
}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Step 4 – Service
public class Service : IElement
{
public string Name { get; }
public decimal Price { get; }
public Service(
string name,
decimal price)
{
Name = name;
Price = price;
}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
15. Tax Visitor
public class TaxVisitor : IVisitor
{
public void Visit(Product product)
{
var tax = product.Price * 0.10m;
Console.WriteLine(
$"Product: {product.Name}");
Console.WriteLine(
$"Price: {product.Price:C}");
Console.WriteLine(
$"Tax: {tax:C}");
}
public void Visit(Service service)
{
var tax = service.Price * 0.15m;
Console.WriteLine(
$"Service: {service.Name}");
Console.WriteLine(
$"Price: {service.Price:C}");
Console.WriteLine(
$"Tax: {tax:C}");
}
}
Here, the tax calculation is outside the domain classes.
16. Report Visitor
We can add another operation without modifying Product or Service.
public class ReportVisitor : IVisitor
{
public void Visit(Product product)
{
Console.WriteLine(
$"Product Report: {product.Name} - {product.Price:C}");
}
public void Visit(Service service)
{
Console.WriteLine(
$"Service Report: {service.Name} - {service.Price:C}");
}
}
This demonstrates one of the primary benefits of Visitor:
New operations can be introduced without modifying the element classes.
17. Program.cs
var elements = new List<IElement>
{
new Product("Laptop", 1200m),
new Product("Monitor", 400m),
new Service("Consulting", 800m)
};
IVisitor taxVisitor = new TaxVisitor();
foreach (var element in elements)
{
element.Accept(taxVisitor);
}
Console.WriteLine();
IVisitor reportVisitor =
new ReportVisitor();
foreach (var element in elements)
{
element.Accept(reportVisitor);
}
The same objects can be processed by different visitors.
18. Execution Flow
When this code executes:
element.Accept(taxVisitor);
the flow is:
Product
|
↓
Accept(taxVisitor)
|
↓
taxVisitor.Visit(this)
|
↓
Visit(Product)
|
↓
Calculate Product Tax
For a service:
Service
|
↓
Accept(taxVisitor)
|
↓
taxVisitor.Visit(this)
|
↓
Visit(Service)
|
↓
Calculate Service Tax
This is the essence of double dispatch.
19. Object Structure
A visitor is often used with a collection or object structure.
For example:
public class ShoppingCart
{
private readonly List<IElement> _items =
new();
public void Add(IElement item)
{
_items.Add(item);
}
public void Accept(IVisitor visitor)
{
foreach (var item in _items)
{
item.Accept(visitor);
}
}
}
Usage:
var cart = new ShoppingCart();
cart.Add(
new Product("Laptop", 1200m));
cart.Add(
new Service("Consulting", 800m));
cart.Accept(
new TaxVisitor());
Now the object structure controls traversal.
20. Shopping Cart Example
Imagine an e-commerce cart containing:
Physical Product
Digital Product
Subscription
Service
We may need operations such as:
Calculate Tax
Calculate Discount
Generate Invoice
Calculate Shipping
Generate Analytics
Instead of adding all these methods to every item, we can create:
TaxVisitor
DiscountVisitor
InvoiceVisitor
ShippingVisitor
AnalyticsVisitor
The architecture becomes:
Shopping Cart
|
+-------------+-------------+
| | |
↓ ↓ ↓
Product Service Subscription
| | |
+-------------+-------------+
|
Accept()
|
↓
Visitor
21. Tax Calculation Example
A tax visitor could implement:
public class TaxCalculationVisitor : IVisitor
{
public void Visit(Product product)
{
Console.WriteLine(
$"Calculating product tax for {product.Name}");
}
public void Visit(Service service)
{
Console.WriteLine(
$"Calculating service tax for {service.Name}");
}
}
The important design decision is that tax calculation does not belong directly to the product or service entity.
This keeps the domain objects focused on their own responsibilities.
22. Document Processing Example
Consider a document processing system:
PdfDocument
WordDocument
ExcelDocument
Operations could include:
Export
Validate
Compress
Audit
Generate Preview
We could create:
ExportVisitor
ValidationVisitor
CompressionVisitor
AuditVisitor
PreviewVisitor
The structure becomes:
Document
|
+-- PDF
+-- Word
+-- Excel
|
↓
Accept()
|
↓
Visitor
23. ASP.NET Core Implementation
Let's create an ASP.NET Core example.
Suppose an e-commerce API has:
Product
Service
Subscription
and we want to calculate different business operations.
24. Element Interface
public interface ICartItem
{
void Accept(ICartVisitor visitor);
}
25. Visitor Interface
public interface ICartVisitor
{
void Visit(Product product);
void Visit(Service service);
void Visit(Subscription subscription);
}
26. Product
public class Product : ICartItem
{
public string Name { get; }
public decimal Price { get; }
public Product(
string name,
decimal price)
{
Name = name;
Price = price;
}
public void Accept(ICartVisitor visitor)
{
visitor.Visit(this);
}
}
27. Service
public class Service : ICartItem
{
public string Name { get; }
public decimal Price { get; }
public Service(
string name,
decimal price)
{
Name = name;
Price = price;
}
public void Accept(ICartVisitor visitor)
{
visitor.Visit(this);
}
}
28. Subscription
public class Subscription : ICartItem
{
public string Name { get; }
public decimal MonthlyPrice { get; }
public Subscription(
string name,
decimal monthlyPrice)
{
Name = name;
MonthlyPrice = monthlyPrice;
}
public void Accept(ICartVisitor visitor)
{
visitor.Visit(this);
}
}
29. Pricing Visitor
public class PricingVisitor : ICartVisitor
{
public decimal Total { get; private set; }
public void Visit(Product product)
{
Total += product.Price;
}
public void Visit(Service service)
{
Total += service.Price;
}
public void Visit(Subscription subscription)
{
Total += subscription.MonthlyPrice;
}
}
30. Registering with Dependency Injection
In ASP.NET Core:
builder.Services.AddScoped<PricingVisitor>();
We can inject the visitor into an application service.
public class CartService
{
private readonly PricingVisitor _pricingVisitor;
public CartService(
PricingVisitor pricingVisitor)
{
_pricingVisitor = pricingVisitor;
}
public decimal CalculateTotal(
IEnumerable<ICartItem> items)
{
foreach (var item in items)
{
item.Accept(_pricingVisitor);
}
return _pricingVisitor.Total;
}
}
In a larger application, it is often preferable for visitors to be stateless or to return results instead of holding mutable request state.
31. Controller Example
[ApiController]
[Route("api/cart")]
public class CartController : ControllerBase
{
private readonly CartService _cartService;
public CartController(
CartService cartService)
{
_cartService = cartService;
}
[HttpPost("calculate")]
public IActionResult Calculate()
{
var items = new List<ICartItem>
{
new Product("Laptop", 1200m),
new Service("Installation", 100m),
new Subscription("Cloud Storage", 25m)
};
var total =
_cartService.CalculateTotal(items);
return Ok(new
{
Total = total
});
}
}
32. Visitor with Dependency Injection
Visitor Pattern and Dependency Injection work well together.
For example, a visitor may depend on:
ILogger
Repository
Tax Service
Configuration
External API Client
Audit Service
Example:
public class AuditVisitor : ICartVisitor
{
private readonly ILogger<AuditVisitor> _logger;
public AuditVisitor(
ILogger<AuditVisitor> logger)
{
_logger = logger;
}
public void Visit(Product product)
{
_logger.LogInformation(
"Product visited: {Name}",
product.Name);
}
public void Visit(Service service)
{
_logger.LogInformation(
"Service visited: {Name}",
service.Name);
}
public void Visit(Subscription subscription)
{
_logger.LogInformation(
"Subscription visited: {Name}",
subscription.Name);
}
}
This makes the Visitor suitable for enterprise scenarios where operations need external services.
33. Real-World Enterprise Example – Tax Calculation
Tax calculation can become complicated when different elements have different rules.
For example:
Product
Service
Subscription
Digital Product
Physical Product
A visitor can encapsulate the tax rules:
TaxVisitor
|
+-- Product Tax
+-- Service Tax
+-- Subscription Tax
+-- Digital Product Tax
This avoids spreading tax calculation logic throughout the domain model.
34. Real-World Enterprise Example – Reporting
Suppose an application contains:
Customer
Order
Invoice
Payment
We need:
Financial Report
Audit Report
Management Report
Export Report
Visitors can represent these operations:
FinancialReportVisitor
AuditVisitor
ManagementReportVisitor
ExportVisitor
The domain model stays relatively stable.
35. Real-World Enterprise Example – AST / Compiler
One of the classic uses of Visitor is processing an Abstract Syntax Tree (AST).
For example:
Expression
|
+-- Addition
+-- Subtraction
+-- Multiplication
+-- Number
Different visitors can perform:
Evaluate
Generate Code
Validate
Pretty Print
Optimize
For example:
Expression Tree
|
↓
EvaluationVisitor
|
↓
Calculate Result
Another visitor can process the same tree:
Expression Tree
|
↓
CodeGenerationVisitor
|
↓
Generate Code
This is one of the strongest examples of where Visitor can be useful.
36. Visitor vs Strategy
Both are behavioral patterns, but their goals differ.
Strategy
Strategy encapsulates an algorithm.
PaymentService
|
↓
PaymentStrategy
|
+----+----+
| | |
Card PayPal Bank
You choose which algorithm to execute.
Visitor
Visitor separates operations from a stable object structure.
Object Structure
|
↓
Accept Visitor
|
↓
Operation
Simple rule:
Strategy changes how something is done. Visitor adds operations to an object structure.
37. Visitor vs Composite
Composite represents a tree-like object structure:
Folder
|
+-- File
+-- Folder
|
+-- File
Visitor can then traverse that structure and perform operations:
FileSizeVisitor
SearchVisitor
SecurityAuditVisitor
In fact, Visitor and Composite are often used together.
38. Visitor vs Interpreter
Interpreter is designed around interpreting a language or grammar.
For example:
Expression
|
+-- Number
+-- Addition
+-- Subtraction
Visitor can be used to perform operations on such expression trees.
A common relationship is:
Interpreter
+
Visitor
where Visitor processes the resulting expression tree.
39. Visitor vs Decorator
Decorator adds behavior around an object:
Object
↓
Decorator
↓
Decorator
↓
Concrete Object
Visitor does not wrap the object.
Instead:
Object
↓
Accept(visitor)
↓
Visitor operation
Simple distinction
Decorator
→ Add behavior to an object
Visitor
→ Add operations to an object structure
40. Advantages
1. Adds New Operations Easily
A new visitor can be introduced without changing existing element classes.
2. Separates Operations from Data
Domain objects don't need to contain every possible operation.
3. Supports Single Responsibility
Business operations can be separated into dedicated visitor classes.
4. Useful for Stable Object Structures
If element types don't change frequently but operations change often, Visitor can be very effective.
5. Works Well with Composite
Visitor is particularly useful when processing hierarchical structures.
6. Centralizes Related Operations
For example:
TaxVisitor
can contain all tax-related logic.
41. Disadvantages
1. Adding New Element Types Is Difficult
Suppose we add:
Subscription
Every visitor interface may need:
void Visit(Subscription subscription);
and every concrete visitor must implement it.
This is the biggest trade-off.
2. Can Increase Complexity
The pattern requires:
Visitor
Concrete Visitor
Element
Concrete Element
Accept()
For simple applications, this may be unnecessary.
3. Tight Coupling Between Visitor and Elements
The visitor interface must know the concrete element types.
4. Double Dispatch Can Be Difficult for Beginners
The Accept() → Visit() flow takes time to understand.
5. Not Ideal for Frequently Changing Object Structures
If new element types are added regularly, maintaining all visitors can become expensive.
42. Best Practices
1. Use Visitor When the Object Structure Is Stable
This is the most important guideline.
2. Use Visitor When Operations Change Frequently
For example:
Stable:
Product
Service
Subscription
Changing:
Tax
Audit
Reporting
Export
Visitor is a strong candidate.
3. Keep Visitors Focused
Prefer:
TaxVisitor
AuditVisitor
ExportVisitor
rather than:
EverythingVisitor
4. Avoid Stateful Visitors When Possible
Prefer visitors that return results or accumulate results in a controlled way.
5. Use Interfaces
For example:
IVisitor
IElement
This improves testability and extensibility.
6. Don't Force Visitor into Simple Applications
If there are only two classes and one operation, a normal method may be better.
7. Combine Visitor with Composite When Appropriate
For hierarchical structures, the combination can be powerful.
43. Common Mistakes
Mistake 1 – Using Visitor When Element Types Change Frequently
If new element types are constantly added, every visitor needs modification.
Mistake 2 – Creating Huge Visitors
A visitor containing hundreds of unrelated operations becomes difficult to maintain.
Mistake 3 – Ignoring Double Dispatch
Developers sometimes misunderstand why:
visitor.Visit(this);
is necessary.
It allows the concrete element type to select the correct overloaded visitor method.
Mistake 4 – Mixing Too Many Responsibilities
Each visitor should represent a meaningful operation.
Mistake 5 – Using Visitor for Simple Business Logic
Don't introduce Visitor just because it is a design pattern.
Use it when the problem actually matches the pattern.
44. Unit Testing Visitor
Visitors are usually straightforward to unit test.
Example:
[Fact]
public void TaxVisitor_ShouldCalculate_ProductTax()
{
var product =
new Product("Laptop", 1000m);
var visitor =
new TaxVisitor();
product.Accept(visitor);
// Assert expected tax behavior.
}
For enterprise applications, visitors can receive mocked dependencies:
TaxVisitor
|
+-- TaxService Mock
+-- Logger Mock
+-- Repository Mock
This allows isolated testing.
45. Interview Questions
Beginner
1. What is the Visitor Design Pattern?
It allows new operations to be added to an object structure without modifying the classes of the objects being operated on.
2. Which category does Visitor belong to?
Behavioral Design Pattern.
3. What are the main components?
Typically:
Visitor
Concrete Visitor
Element
Concrete Element
Object Structure
4. What is Accept()?
It is the method through which an element accepts a visitor.
void Accept(IVisitor visitor);
5. What is Visit()?
It is the visitor operation for a particular concrete element.
Intermediate
6. What is Double Dispatch?
Double Dispatch is a technique where the final method selected depends on both the runtime element and visitor types.
7. Why is visitor.Visit(this) used?
It passes the concrete element to the visitor, allowing the appropriate overloaded Visit() method to be selected.
8. What is the biggest advantage of Visitor?
Adding new operations is relatively easy without modifying existing element classes.
9. What is the biggest disadvantage?
Adding a new element type requires changes to the visitor interface and usually all concrete visitors.
10. Visitor vs Strategy?
Strategy encapsulates interchangeable algorithms.
Visitor separates operations from a relatively stable object structure.
11. Visitor vs Decorator?
Decorator wraps objects to add behavior.
Visitor performs operations on objects without wrapping them.
12. Can Visitor work with Composite?
Yes. Visitor is frequently used with Composite to traverse and process tree structures.
Advanced
13. When should you use Visitor?
When:
Object types are relatively stable
AND
Operations change frequently
14. When should you avoid Visitor?
When new element types are added frequently or the object structure is unstable.
15. Can Visitor use Dependency Injection?
Yes. Visitors can receive repositories, services, loggers, APIs, and other dependencies through ASP.NET Core DI.
16. Why is Visitor considered an advanced pattern?
Because it involves:
Polymorphism
Overloading
Double Dispatch
Object Structures
Separation of Operations
17. Can Visitor return a value?
Yes.
Instead of:
void Visit(Product product)
you can design visitors around results, for example:
decimal Visit(Product product);
or use a result object.
The appropriate approach depends on the operation.
18. Is Visitor always better than adding methods to the classes?
No.
Visitor is useful only when its trade-offs match the application's structure.
46. When Should You Use Visitor?
Visitor is a good choice when:
The object structure is relatively stable.
Many operations need to be performed on those objects.
New operations are added frequently.
You want to separate business operations from domain objects.
You need to process tree structures.
You need different algorithms over the same object hierarchy.
Typical examples:
AST Processing
Document Processing
Tax Calculation
Reporting
Auditing
Code Generation
File System Traversal
Shopping Cart Processing
Analytics
Validation
47. When Should You Avoid Visitor?
Avoid Visitor when:
Element types change frequently.
The object model is very small.
Only one or two operations exist.
The pattern would introduce unnecessary complexity.
Simple polymorphism can solve the problem.
Strategy or another pattern is a better fit.
48. Visitor Pattern in Enterprise Architecture
A possible enterprise architecture is:
ASP.NET Core API
|
↓
Application Layer
|
↓
Object Structure
|
+-------------+-------------+
| | |
↓ ↓ ↓
Product Service Subscription
| | |
+-------------+-------------+
|
Accept()
|
↓
Visitor
|
+----------------+----------------+
| | |
↓ ↓ ↓
TaxVisitor AuditVisitor ReportVisitor
This can help keep business operations separate from domain objects.
49. Visitor + Composite
A powerful combination is:
Composite
+
Visitor
For example, a company hierarchy:
Company
|
+-- Department
| |
| +-- Employee
| +-- Employee
|
+-- Department
|
+-- Employee
+-- Employee
Visitors can perform:
SalaryVisitor
AuditVisitor
ReportVisitor
PerformanceVisitor
The Composite handles the hierarchy.
The Visitor handles the operation.
50. Visitor + Dependency Injection + ASP.NET Core
A modern .NET application can combine:
ASP.NET Core
↓
Dependency Injection
↓
Application Service
↓
Visitor
↓
Domain Objects
For example:
POST /api/orders/calculate-tax
|
↓
OrderController
|
↓
OrderService
|
↓
TaxVisitor
|
+-------+-------+
| | |
↓ ↓ ↓
Product Service Subscription
This can be particularly useful when visitor operations require infrastructure services.
51. Simple Memory Trick
Remember Visitor using this phrase:
"Keep the objects stable, move the operations outside."
For example:
Stable:
Product
Service
Subscription
Operations:
Tax
Audit
Report
Export
Move operations into visitors:
TaxVisitor
AuditVisitor
ReportVisitor
ExportVisitor
52. Visitor vs Other Behavioral Patterns
| Pattern | Main Purpose |
|---|---|
| Chain of Responsibility | Pass request through handlers |
| Command | Encapsulate a request as an object |
| Interpreter | Interpret a language/grammar |
| Iterator | Traverse a collection |
| Mediator | Centralize communication |
| Memento | Capture and restore state |
| Observer | Notify dependent objects |
| State | Change behavior based on state |
| Strategy | Encapsulate interchangeable algorithms |
| Template Method | Define algorithm skeleton |
| Visitor | Add operations to object structures |
53. Complete Visitor Flow
The complete flow can be remembered as:
Object Structure
|
↓
Concrete Element
|
↓
Accept(visitor)
|
↓
visitor.Visit(this)
|
↓
Concrete Visitor
|
↓
Perform Operation
For example:
Product
|
↓
Accept(TaxVisitor)
|
↓
TaxVisitor.Visit(Product)
|
↓
Calculate Product Tax
54. Key Takeaways
Remember these important points:
1. Visitor is a Behavioral Design Pattern
It focuses on operations performed across object structures.
2. Visitor separates operations from objects
Instead of adding every operation to domain classes, operations can be implemented in visitors.
3. Accept() is critical
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
4. Double Dispatch is central to the pattern
The selected operation depends on both the visitor and concrete element type.
5. Visitor is excellent when the object structure is stable
For example:
Product
Service
Subscription
while operations frequently change:
Tax
Audit
Report
Export
6. Visitor makes adding operations easier
A new visitor can represent a new operation.
7. Adding element types is expensive
If we add:
NewElement
the visitor interface and all concrete visitors may need modification.
8. Visitor and Composite work well together
Visitor can traverse and operate on hierarchical structures.
55. Conclusion
The Visitor Design Pattern provides a powerful way to separate operations from the objects on which those operations operate.
The fundamental idea is:
Object Structure
|
↓
Accept Visitor
|
↓
Visitor performs operation
Instead of continuously adding business operations to domain classes:
Product
├── Tax
├── Audit
├── Reporting
├── Export
└── Validation
we can separate them:
Product
Service
Subscription
|
↓
Accept()
|
+── TaxVisitor
+── AuditVisitor
+── ReportVisitor
+── ExportVisitor
This can make the design cleaner when the object structure is stable but operations change frequently.
However, Visitor is not a universal solution. If your application frequently adds new element types, the pattern can become expensive because every visitor must be updated.
The key rule to remember is:
Use Visitor when you have a stable object structure and need to frequently add new operations across that structure.
🎯 Behavioral Design Patterns – Complete!
With Part 4.11, we have now covered the major Behavioral Design Patterns in this series:
Part 4.1 → Chain of Responsibility
Part 4.2 → Command
Part 4.3 → Interpreter
Part 4.4 → Iterator
Part 4.5 → Mediator
Part 4.6 → Memento
Part 4.7 → Observer
Part 4.8 → State
Part 4.9 → Strategy
Part 4.10 → Template Method
Part 4.11 → Visitor
You have now completed:
DESIGN PATTERNS
|
+------------+------------+
| | |
↓ ↓ ↓
Creational Structural Behavioral
| | |
5 Patterns 7 Patterns 11 Patterns
That's 23 classic GoF Design Patterns covered across the series.
🚀 Coming Up Next
Part 5 – Design Patterns in Real-World .NET Architecture
The next section can bring everything together by showing how these patterns are actually used in enterprise applications.
Possible topics include:
Design Patterns in Clean Architecture
Design Patterns in Microservices
Design Patterns in ASP.NET Core Web APIs
Design Patterns with Entity Framework Core
Design Patterns with CQRS
Design Patterns with MediatR
Design Patterns with Azure Services
Design Patterns in Event-Driven Architecture
Design Patterns in E-Commerce Systems
Design Patterns in Banking Applications
Combining Multiple Design Patterns
Common Design Pattern Anti-Patterns
How to Choose the Right Design Pattern
Design Patterns Interview Guide
Complete Real-World .NET Architecture Case Study
The next section will focus less on learning patterns individually and more on understanding how experienced .NET developers and architects combine multiple patterns to build maintainable, scalable, and enterprise-ready applications.
