Status: ✅ Phase 2 - Complete (100% - Visitor Pattern Implemented) Timeline: 3 weeks actual (vs 3 weeks planned) Breaking Changes: Internal only (legacy path removed) Created: 2025-01-08 Last Updated: 2025-11-09
This document outlines the comprehensive refactoring that was performed for Templify to address architectural limitations and improve extensibility. The critical limitation where conditionals could not be used inside loops has been completely resolved through the visitor pattern implementation.
- ✅ Not in production - Breaking changes are acceptable
- ✅ Need it now - High priority to enable conditionals in loops
- ✅ Step-by-step approach - 5-week phased implementation
- ✅ Public API changes allowed - If they improve the design
- Enable conditionals inside loops - The #1 critical limitation
- Eliminate code duplication - DRY principle violations
- Introduce proper abstractions - IEvaluationContext, ITemplateProcessor
- Make pipeline extensible - Support future features easily
| Phase | Duration | Goal | Status | Notes |
|---|---|---|---|---|
| Phase 1 | Week 1 (planned) | Enable conditionals in loops | ✅ Complete | IEvaluationContext abstraction |
| Phase 2 | Weeks 2-3 (planned) | Visitor pattern + clean architecture | ✅ Complete | Legacy path removed entirely |
| Phase 3 | Weeks 4-5 (planned) | Full extensibility + pipeline | ⏸️ Deferred | Not needed for current requirements |
Phase 2 Completion (2025-11-09):
- ✅ Full visitor pattern implementation (DocumentWalker, ConditionalVisitor, LoopVisitor, PlaceholderVisitor)
- ✅ Composite visitor for flexible composition
- ✅ Table row loop support (loops spanning table rows)
- ✅ Nested loop support (arbitrary nesting depth)
- ✅ Legacy path completely removed (went beyond plan - no dual-mode support)
- ✅ All 319 tests passing (100% success rate)
- ✅ Code duplication eliminated (~300 lines removed)
Differences from Original Plan:
- Removed
ProcessingModeenum and dual-path support (Legacy vs ContextAware) - Simplified API - single processing path using visitor pattern
- Added
TableRowhandling in DocumentWalker for proper table row loop support - Breaking Change: Removed legacy processing entirely (acceptable - not in production)
- Current Architecture Analysis
- Identified Issues
- Phase 1: Evaluation Context Abstraction
- Phase 2: Visitor Pattern Refactoring
- Phase 3: Extensible Pipeline
- Testing Strategy
- Migration Guide
- Success Metrics
File: DocumentTemplateProcessor.cs (lines 78-90)
Current Order (hard-coded):
1. ConditionalProcessor → Process {{#if}}/{{#else}}/{{/if}} globally
2. LoopDetector + LoopProcessor → Process {{#foreach}}/{{/foreach}}
3. DocumentBodyReplacer → Replace simple placeholders in paragraphs
4. TableReplacer → Replace placeholders in tables
Data Flow:
Template Document (Stream)
↓
1. ConditionalProcessor.ProcessConditionals()
- Evaluates ALL conditionals using ROOT data only
- Removes false branches globally
- All {{#if}} markers removed from document
↓
2. LoopProcessor.ProcessLoops()
- Creates LoopContext for each iteration
- Clones content for each item
- Processes placeholders with loop-scoped data
- Detects and processes NESTED loops recursively
↓
3. DocumentBodyReplacer.ReplaceInBody()
- Replaces remaining placeholders using root data
↓
4. TableReplacer.ReplaceInTables()
- Replaces table placeholders
↓
Output Document (Stream)
- Purpose: Entry point, coordinates all processing
- Responsibilities:
- Opens/copies document streams
- Instantiates all processors
- Executes processing pipeline in fixed order
- Aggregates results and missing variables
- Purpose: Handle
{{#if}}/{{#else}}/{{/if}}blocks - Responsibilities:
- Detect conditional blocks (supports nesting)
- Evaluate expressions using ConditionalEvaluator
- Remove false branches, keep true branches
- Support table row conditionals
Strengths:
- ✅ Handles nested conditionals correctly (deepest-first)
- ✅ Supports complex expressions (and, or, not, comparisons)
- ✅ Clean separation: detection → evaluation → removal
Issues:
- ❌ Only accesses global/root data
- ❌ Cannot evaluate conditions inside loop iterations
- ❌ Processed before loops, so loop variables unavailable
- Purpose: Handle
{{#foreach Collection}}/{{/foreach}}blocks - Responsibilities:
- Detect loop blocks
- Create LoopContext with metadata (@index, @first, @last, @count)
- Clone content for each iteration
- Support nested loops with context chaining
- Process placeholders within loop scope
Strengths:
- ✅ Excellent nested loop support
- ✅ Rich loop metadata
- ✅ Proper context chaining for nested structures
Issues:
- ❌ Duplicates placeholder processing logic from DocumentBodyReplacer
- ❌ Duplicates detection logic (TryDetectLoop vs LoopDetector)
- ❌ Cannot process conditionals inside loop iterations
- Purpose: Resolve variable values from data
- Responsibilities:
- ValueResolver: Global scope, nested property paths (Customer.Address.City)
- LoopContext: Loop scope, with parent context chaining
Issues:
- ❌ Two separate resolution systems (not unified)
- ❌ ConditionalEvaluator cannot use LoopContext
1. Placeholder Processing (near-identical code):
LoopProcessor.ProcessParagraph()(lines 146-203)DocumentBodyReplacer.ProcessParagraph()(lines 57-126)
2. Element Text Extraction (identical implementations):
ConditionalDetector.GetElementText()(lines 189-207)LoopDetector.GetElementText()(lines 150-168)LoopProcessor.GetElementText()(lines 536-554)
3. Detection Logic (duplicate patterns):
LoopDetector.DetectLoopsInElements()(lines 59-112)LoopProcessor.TryDetectLoop()(lines 360-402)
Problem: Conditionals are processed BEFORE loops, making conditional logic inside loop iterations impossible.
Example that FAILS:
{{#foreach Orders}}
Order #{{OrderId}}
{{#if Amount > 1000}}
🔥 HIGH VALUE: {{Amount}} EUR
{{#else}}
Standard: {{Amount}} EUR
{{/if}}
{{/foreach}}
Why it fails:
- Step 1: ConditionalProcessor evaluates
Amount > 1000using ROOT data - At this point, we're NOT inside any loop, so
Amount= root-level variable (if exists) or missing - Conditional branch is selected ONCE globally
- Step 2: LoopProcessor clones the selected branch for each order
- Result: All orders get the same conditional result (not per-order evaluation)
Current workaround: None - feature is not supported
Impact: Major limitation for report generation use cases
Duplication Impact:
- Harder to maintain (changes needed in multiple places)
- Higher bug risk (fix in one place, forget another)
- Inconsistent behavior potential
- Larger codebase
Estimated Duplication:
- Placeholder processing: ~150 lines duplicated
- Detection logic: ~100 lines duplicated
- Helper methods: ~50 lines duplicated
- Total: ~300 lines of duplicate code
No Unified Evaluation Context:
- Global data:
Dictionary<string, object> - Loop context:
LoopContext(separate class) - No shared interface or abstraction
- ConditionalEvaluator cannot work with LoopContext
No Processor Interface:
- Each processor has different method signatures
- Cannot compose or reorder processors dynamically
- Hard to test in isolation
- No plugin/extension mechanism
No Template Element Abstraction:
- Working directly with OpenXmlElement
- No domain model for template constructs
- Tight coupling to OpenXML API
Current State: Adding new features requires:
- Modifying
DocumentTemplateProcessorconstructor - Adding new processing step to hardcoded pipeline
- Determining execution order vs existing steps
- Risk of conflicts with existing processors
Examples of difficult-to-add features:
- Custom functions:
{{#if FormatDate(OrderDate) = "2025-01-01"}} - Filter expressions:
{{#foreach Orders where Amount > 1000}} Headers/footers: Would need separate code path(implemented - reuses existing visitor pipeline viaDocumentWalker.WalkHeadersAndFooters)- Partial templates:
{{> include "header.docx"}} - Custom blocks:
{{#custom MyBlock}}...{{/custom}}
Timeline: Week 1 (5 working days) Goal: Enable conditionals inside loops with minimal changes Breaking Changes: Internal only
Purpose: Unify variable resolution across all processors
namespace TriasDev.Templify;
/// <summary>
/// Represents a context for evaluating variables in template expressions.
/// Supports hierarchical contexts (e.g., loop contexts nested in global context).
/// </summary>
public interface IEvaluationContext
{
/// <summary>
/// Tries to resolve a variable by name.
/// </summary>
/// <param name="variableName">Variable name (supports dot notation like "Customer.Name")</param>
/// <param name="value">Resolved value if found</param>
/// <returns>True if variable was found, false otherwise</returns>
bool TryResolveVariable(string variableName, out object? value);
/// <summary>
/// Gets the parent context (for nested contexts), or null for root context.
/// </summary>
IEvaluationContext? Parent { get; }
/// <summary>
/// Gets the root data dictionary (useful for metadata access).
/// </summary>
IReadOnlyDictionary<string, object> RootData { get; }
}Purpose: Root-level evaluation using dictionary data
namespace TriasDev.Templify;
/// <summary>
/// Global evaluation context that resolves variables from root data dictionary.
/// </summary>
public sealed class GlobalEvaluationContext : IEvaluationContext
{
private readonly Dictionary<string, object> _data;
private readonly ValueResolver _valueResolver;
public GlobalEvaluationContext(Dictionary<string, object> data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
_valueResolver = new ValueResolver();
}
public bool TryResolveVariable(string variableName, out object? value)
{
return _valueResolver.TryResolveValue(_data, variableName, out value);
}
public IEvaluationContext? Parent => null;
public IReadOnlyDictionary<string, object> RootData => _data;
}Purpose: Loop-scoped evaluation with fallback to parent
namespace TriasDev.Templify;
/// <summary>
/// Loop evaluation context that resolves variables from loop iteration context,
/// with fallback to parent context.
/// </summary>
public sealed class LoopEvaluationContext : IEvaluationContext
{
private readonly LoopContext _loopContext;
private readonly IEvaluationContext _parent;
public LoopEvaluationContext(LoopContext loopContext, IEvaluationContext parent)
{
_loopContext = loopContext ?? throw new ArgumentNullException(nameof(loopContext));
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
}
public bool TryResolveVariable(string variableName, out object? value)
{
// Try loop context first (handles @index, @first, @last, @count, current item properties)
if (_loopContext.TryResolveVariable(variableName, out value))
return true;
// Fall back to parent context (root data or parent loop)
return _parent.TryResolveVariable(variableName, out value);
}
public IEvaluationContext? Parent => _parent;
public IReadOnlyDictionary<string, object> RootData => _parent.RootData;
}File: ConditionalEvaluator.cs
Before:
public bool Evaluate(string expression, Dictionary<string, object> data)
{
// Uses ValueResolver directly with data dictionary
// ...
}After:
public bool Evaluate(string expression, IEvaluationContext context)
{
// Uses context.TryResolveVariable() instead
// ...
}Impact: Internal change, public API unchanged
File: ConditionalProcessor.cs
Before:
public void ProcessConditionals(WordprocessingDocument document, Dictionary<string, object> data)
{
// Creates global context implicitly
// ...
}After:
public void ProcessConditionals(WordprocessingDocument document, IEvaluationContext context)
{
// Accepts context parameter
// Can work in both global and loop scopes
// ...
}
// Overload for backward compatibility (can be removed later)
public void ProcessConditionals(WordprocessingDocument document, Dictionary<string, object> data)
{
var context = new GlobalEvaluationContext(data);
ProcessConditionals(document, context);
}File: DocumentTemplateProcessor.cs
Before:
// Step 0: Process conditionals (must be done before loops)
_conditionalProcessor.ProcessConditionals(document, data);After:
// Step 0: Process conditionals at global scope
var globalContext = new GlobalEvaluationContext(data);
_conditionalProcessor.ProcessConditionals(document, globalContext);File: LoopProcessor.cs
Current: Lines 319-355 detect and process nested loops Add: Lines ~356-390 detect and process nested conditionals
New Method:
private void ProcessConditionals(
List<OpenXmlElement> clonedContent,
LoopContext context,
List<string> missingVariables)
{
// Create loop evaluation context
var loopEvalContext = new LoopEvaluationContext(context, _globalContext);
// Detect conditionals in cloned content
var conditionals = ConditionalDetector.DetectConditionals(clonedContent);
if (conditionals.Count == 0)
return;
// Process conditionals with loop-scoped context
foreach (var conditional in conditionals.OrderByDescending(c => c.NestingLevel))
{
bool result = _conditionalEvaluator.Evaluate(
conditional.ConditionExpression,
loopEvalContext
);
// Remove false branch, keep true branch
// (same logic as ConditionalProcessor)
// ...
}
}Integration Point: Call after nested loop processing (line ~355):
// Process nested loops in cloned content
ProcessNestedLoops(clonedContent, context, data, missingVariables);
// NEW: Process conditionals in cloned content with loop context
ProcessConditionals(clonedContent, context, missingVariables);
// Process placeholders in cloned content
// ...- Create
IEvaluationContext.cs - Create
GlobalEvaluationContext.cs - Create
LoopEvaluationContext.cs - Add unit tests for context implementations
- Update
ConditionalEvaluator.Evaluate()to acceptIEvaluationContext - Update all evaluator tests to use contexts
- Verify no regressions in conditional logic
- Update
ProcessConditionals()to accept context - Update
DocumentTemplateProcessorto create GlobalEvaluationContext - Verify top-level conditionals still work
- Add
ProcessConditionals()method to LoopProcessor - Integrate after nested loop processing
- Store global context reference in LoopProcessor
- Create integration test: conditional inside loop
- Create integration test: nested conditionals inside loops
- Create integration test: conditionals with loop metadata (@index, @first, etc.)
- Fixed critical bug: LoopProcessor now modifies clonedElements directly (Remove() was failing silently)
Test 1: Simple Conditional in Loop
[Fact]
public void ProcessTemplate_ConditionalInsideLoop_EvaluatesPerIteration()
{
// Template:
// {{#foreach Orders}}
// {{#if Amount > 1000}}High Value{{#else}}Standard{{/if}}
// {{/foreach}}
// Data:
// Orders = [{ Amount: 500 }, { Amount: 1500 }, { Amount: 800 }]
// Expected Output:
// Standard
// High Value
// Standard
}Test 2: Conditional with Loop Metadata
[Fact]
public void ProcessTemplate_ConditionalWithLoopMetadata_Works()
{
// Template:
// {{#foreach Items}}
// {{#if @first}}First: {{.}}{{#else}}Item: {{.}}{{/if}}
// {{/foreach}}
// Expected Output:
// First: Apple
// Item: Banana
// Item: Cherry
}Test 3: Nested Loops with Conditionals
[Fact]
public void ProcessTemplate_ConditionalInNestedLoop_Works()
{
// Template:
// {{#foreach Categories}}
// {{Name}}:
// {{#foreach Products}}
// {{#if Price < 50}}• {{Name}} (Budget){{#else}}• {{Name}}{{/if}}
// {{/foreach}}
// {{/foreach}}
}- ✅ All existing tests pass (no regression)
- ✅ 3+ new integration tests for conditionals in loops pass
- ✅ Performance impact < 5% (benchmark before/after)
- ✅ Code coverage maintained or improved
- ✅ Documentation updated (README.md limitation removed)
⚠️ Still has code duplication (addressed in Phase 2)⚠️ Conditional processing logic in two places (ConditionalProcessor and LoopProcessor)⚠️ No extensibility improvements (addressed in Phase 3)
Timeline: Weeks 2-3 (10 working days planned) - ✅ COMPLETED Goal: Clean architecture with visitor pattern - ✅ ACHIEVED Breaking Changes: Internal only (legacy path removed entirely)
Problem: Currently, we have:
- ConditionalProcessor: Processes conditionals globally
- LoopProcessor: Processes loops, also processes conditionals locally
- Duplication and unclear separation of concerns
Solution: Visitor pattern
- DocumentWalker: Traverses document tree
- ITemplateElementVisitor: Processes template constructs
- Visitors: ConditionalVisitor, LoopVisitor, PlaceholderVisitor
- Context-aware: Each visitor receives IEvaluationContext
namespace TriasDev.Templify;
/// <summary>
/// Visitor interface for processing template elements.
/// </summary>
public interface ITemplateElementVisitor
{
/// <summary>
/// Visits a conditional block ({{#if}}/{{#else}}/{{/if}}).
/// </summary>
void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context);
/// <summary>
/// Visits a loop block ({{#foreach}}/{{/foreach}}).
/// </summary>
void VisitLoopBlock(LoopBlock block, IEvaluationContext context);
/// <summary>
/// Visits a placeholder ({{VariableName}}).
/// </summary>
void VisitPlaceholder(PlaceholderMatch placeholder, IEvaluationContext context);
/// <summary>
/// Visits a regular paragraph (no template constructs).
/// </summary>
void VisitParagraph(Paragraph paragraph, IEvaluationContext context);
}Purpose: Traverses document and dispatches to visitors
namespace TriasDev.Templify;
/// <summary>
/// Walks through document elements and dispatches to appropriate visitor methods.
/// </summary>
public sealed class DocumentWalker
{
public void Walk(
IEnumerable<OpenXmlElement> elements,
ITemplateElementVisitor visitor,
IEvaluationContext context)
{
foreach (var element in elements.ToList()) // ToList to avoid modification during iteration
{
// Detect what kind of template construct this is
if (TryDetectConditional(element, out var conditional))
{
visitor.VisitConditionalBlock(conditional, context);
continue;
}
if (TryDetectLoop(element, out var loop))
{
visitor.VisitLoopBlock(loop, context);
continue;
}
if (TryDetectPlaceholder(element, out var placeholder))
{
visitor.VisitPlaceholder(placeholder, context);
continue;
}
if (element is Paragraph paragraph)
{
visitor.VisitParagraph(paragraph, context);
}
// Recursively walk child elements
if (element.HasChildren)
{
Walk(element.Elements(), visitor, context);
}
}
}
private bool TryDetectConditional(OpenXmlElement element, out ConditionalBlock block)
{
// Use ConditionalDetector logic
// ...
}
private bool TryDetectLoop(OpenXmlElement element, out LoopBlock block)
{
// Use LoopDetector logic
// ...
}
private bool TryDetectPlaceholder(OpenXmlElement element, out PlaceholderMatch match)
{
// Use placeholder regex
// ...
}
}Purpose: Processes conditional blocks with context awareness
namespace TriasDev.Templify;
public sealed class ConditionalVisitor : ITemplateElementVisitor
{
private readonly ConditionalEvaluator _evaluator;
public void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context)
{
// Evaluate condition with current context (could be global OR loop-scoped!)
bool result = _evaluator.Evaluate(block.ConditionExpression, context);
if (result)
{
// Remove ELSE branch, keep IF branch
block.RemoveElseBranch();
}
else
{
// Remove IF branch, keep ELSE branch
block.RemoveIfBranch();
}
// Remove conditional markers
block.RemoveMarkers();
}
public void VisitLoopBlock(LoopBlock block, IEvaluationContext context)
{
// Not handled by this visitor
}
public void VisitPlaceholder(PlaceholderMatch placeholder, IEvaluationContext context)
{
// Not handled by this visitor
}
public void VisitParagraph(Paragraph paragraph, IEvaluationContext context)
{
// Not handled by this visitor
}
}Purpose: Processes loop blocks and creates nested contexts
namespace TriasDev.Templify;
public sealed class LoopVisitor : ITemplateElementVisitor
{
private readonly DocumentWalker _walker;
private readonly ITemplateElementVisitor _nestedVisitor;
public void VisitLoopBlock(LoopBlock block, IEvaluationContext context)
{
// Get collection to loop over
if (!context.TryResolveVariable(block.CollectionName, out var collectionObj))
return; // Collection not found
IEnumerable<object?> collection = ConvertToEnumerable(collectionObj);
// Clone content for each iteration
List<OpenXmlElement> allClonedContent = new();
int index = 0;
foreach (var item in collection)
{
// Create loop context for this iteration
var loopContext = new LoopContext(item, index, collection, context.RootData);
var loopEvalContext = new LoopEvaluationContext(loopContext, context);
// Clone content
var clonedContent = CloneElements(block.ContentElements);
// Walk cloned content with loop context
// This will process nested loops, conditionals, and placeholders
_walker.Walk(clonedContent, _nestedVisitor, loopEvalContext);
allClonedContent.AddRange(clonedContent);
index++;
}
// Replace loop block with all cloned content
block.ReplaceWith(allClonedContent);
}
// Other Visit methods...
}Purpose: Combines multiple visitors
namespace TriasDev.Templify;
/// <summary>
/// Composite visitor that delegates to multiple visitors.
/// </summary>
public sealed class CompositeVisitor : ITemplateElementVisitor
{
private readonly List<ITemplateElementVisitor> _visitors;
public CompositeVisitor(params ITemplateElementVisitor[] visitors)
{
_visitors = new List<ITemplateElementVisitor>(visitors);
}
public void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context)
{
foreach (var visitor in _visitors)
visitor.VisitConditionalBlock(block, context);
}
// Similar for other Visit methods...
}public enum ProcessingMode
{
/// <summary>
/// Legacy mode: Conditionals processed globally before loops.
/// Maintains backward compatibility but conditionals inside loops don't work.
/// </summary>
Legacy = 0,
/// <summary>
/// Context-aware mode: Uses visitor pattern with context-aware processing.
/// Conditionals inside loops work correctly. Recommended for new templates.
/// </summary>
ContextAware = 1
}
public sealed class PlaceholderReplacementOptions
{
// Existing properties...
/// <summary>
/// Gets or sets the processing mode.
/// Default is Legacy for backward compatibility.
/// </summary>
public ProcessingMode ProcessingMode { get; init; } = ProcessingMode.Legacy;
}- Create
ITemplateElementVisitor.cs - Create
DocumentWalker.cs - Create
ConditionalVisitor.cs - Create
LoopVisitor.cs - Create
PlaceholderVisitor.cs - Create
CompositeVisitor.cs
public ProcessingResult ProcessTemplate(
Stream templateStream,
Stream outputStream,
Dictionary<string, object> data)
{
// ...
if (_options.ProcessingMode == ProcessingMode.ContextAware)
{
// New code path: Use visitor pattern
var globalContext = new GlobalEvaluationContext(data);
var walker = new DocumentWalker();
var visitor = new CompositeVisitor(
new ConditionalVisitor(),
new LoopVisitor(walker),
new PlaceholderVisitor()
);
walker.Walk(document.MainDocumentPart.Document.Body.Elements(), visitor, globalContext);
}
else
{
// Legacy code path: Keep existing order
// (Phase 1 code)
}
// ...
}Create TemplateElementHelper.cs:
public static class TemplateElementHelper
{
public static string GetElementText(OpenXmlElement element)
{
// Move duplicate implementations here
}
public static OpenXmlElement CloneElement(OpenXmlElement element)
{
// Shared cloning logic
}
// Other shared helpers...
}- Day 1-2: Create visitor interfaces and DocumentWalker
- Day 3-4: Implement ConditionalVisitor and LoopVisitor
- Day 5: Implement PlaceholderVisitor and CompositeVisitor
- Day 1-2: Integrate ContextAware mode into DocumentTemplateProcessor
- Day 3: Extract shared utilities, remove duplication
- Day 4: Comprehensive testing (both Legacy and ContextAware modes)
- Day 5: Performance testing, documentation updates
Status: ✅ Complete - 319/319 tests passing
Commits:
feat: integrate visitor pattern with legacy path coexistence- Implemented dual-path processingrefactor: remove legacy processing path from DocumentTemplateProcessor- Removed ~180 lines of duplicate codefix: complete visitor pattern implementation with table row loops and nested loops- Fixed final 3 failing tests
What Was Implemented:
-
Visitor Infrastructure (
Visitors/folder):ITemplateElementVisitor.cs- Visitor interface with methods for each template element typeDocumentWalker.cs- Traverses document tree and dispatches to visitorsConditionalVisitor.cs- Processes {{#if}}/{{#else}}/{{/if}} blocksLoopVisitor.cs- Processes {{#foreach}}/{{/foreach}} blocks with recursive nesting supportPlaceholderVisitor.cs- Replaces {{Variable}} placeholders with valuesCompositeVisitor.cs- Combines multiple visitors for unified processing
-
DocumentTemplateProcessor Changes:
- Removed legacy processing path entirely (ConditionalProcessor, DocumentBodyReplacer, TableReplacer calls)
- Simplified to single visitor-based processing path
- Composite visitor composition for proper nested loop handling
- No
ProcessingModeenum needed - single path for all scenarios
-
Table Row Loop Support:
- Made
LoopDetector.DetectTableRowLoops()internal for use by DocumentWalker - Modified
DocumentWalker.WalkTable()to detect table row loops at table level - Added
TableRowhandling inDocumentWalker.WalkElements()for processing cloned table row content
- Made
-
Nested Loop Support:
- Fixed visitor composition in DocumentTemplateProcessor to support arbitrary nesting
- LoopVisitor receives a composite that includes another LoopVisitor for recursive processing
- Inner loops are detected and processed within cloned content of outer loops
Key Architectural Decisions:
- No Dual-Mode: Instead of having Legacy + ContextAware modes, we removed the legacy path entirely
- Single Composite: All visitors (conditional, loop, placeholder) are in a single composite
- Recursive Composition: LoopVisitor uses the same composite for processing cloned content, enabling nested loops
- Table-First Detection: Table row loops are detected at table level before cell-by-cell traversal
Test Results:
- ✅ All 319 tests passing (100% success rate)
- ✅ Simple placeholders work
- ✅ Conditionals (including nested) work
- ✅ Loops (including nested) work
- ✅ Table row loops work
- ✅ Conditionals inside loops work
- ✅ Complex combinations work
Code Metrics:
- Lines Removed: ~300 lines (legacy processing, duplication, unused features)
- Lines Added: ~400 lines (visitor pattern infrastructure)
- Net Change: +100 lines with significantly better architecture
- Code Duplication: Eliminated (was ~40% of core logic)
- Test Coverage: Maintained at 100% passing
Each integration test should have two variants:
[Theory]
[InlineData(ProcessingMode.Legacy)]
[InlineData(ProcessingMode.ContextAware)]
public void ProcessTemplate_ConditionalInLoop_Works(ProcessingMode mode)
{
var options = new PlaceholderReplacementOptions
{
ProcessingMode = mode
};
// Test conditionals in loops
// Should pass for ContextAware, skip/fail for Legacy
}- All existing tests must pass in Legacy mode
- Performance within 5% of Phase 1
- ContextAware mode enables new scenarios
- ✅ All existing tests pass in Legacy mode
- ✅ Conditionals in loops work in ContextAware mode
- ✅ Code duplication eliminated (~300 lines removed)
- ✅ Performance impact < 5% in ContextAware mode
- ✅ Clean architecture with proper separation of concerns
Timeline: Weeks 4-5 (10 working days) Goal: Full extensibility with plugin support Breaking Changes: Public API enhancements (additive)
Purpose: Standard interface for all processors
namespace TriasDev.Templify;
/// <summary>
/// Interface for template processors that can be composed into a pipeline.
/// </summary>
public interface ITemplateProcessor
{
/// <summary>
/// Gets the processor name (for diagnostics and logging).
/// </summary>
string Name { get; }
/// <summary>
/// Processes a portion of the document template.
/// </summary>
/// <param name="document">The Word document to process</param>
/// <param name="context">The evaluation context for variable resolution</param>
/// <param name="options">Processing options</param>
/// <returns>Result containing success status, replacements, and missing variables</returns>
ProcessorResult Process(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options);
}
/// <summary>
/// Result of a processor execution.
/// </summary>
public sealed class ProcessorResult
{
public bool Success { get; init; }
public int ReplacementCount { get; init; }
public IReadOnlyList<string> MissingVariables { get; init; } = Array.Empty<string>();
public string? ErrorMessage { get; init; }
/// <summary>
/// If false, stops the pipeline (useful for validation processors).
/// </summary>
public bool ContinueProcessing { get; init; } = true;
public static ProcessorResult Ok(int replacements = 0, IReadOnlyList<string>? missing = null)
=> new() { Success = true, ReplacementCount = replacements, MissingVariables = missing ?? Array.Empty<string>() };
public static ProcessorResult Failure(string error)
=> new() { Success = false, ErrorMessage = error, ContinueProcessing = false };
}Purpose: Composable processor pipeline
namespace TriasDev.Templify;
/// <summary>
/// Pipeline for composing multiple template processors.
/// </summary>
public sealed class TemplateProcessorPipeline
{
private readonly List<ITemplateProcessor> _processors = new();
/// <summary>
/// Adds a processor to the pipeline.
/// Processors are executed in the order they are added.
/// </summary>
public TemplateProcessorPipeline AddProcessor(ITemplateProcessor processor)
{
_processors.Add(processor ?? throw new ArgumentNullException(nameof(processor)));
return this;
}
/// <summary>
/// Removes all processors of a specific type.
/// </summary>
public TemplateProcessorPipeline RemoveProcessors<T>() where T : ITemplateProcessor
{
_processors.RemoveAll(p => p is T);
return this;
}
/// <summary>
/// Inserts a processor before another processor type.
/// </summary>
public TemplateProcessorPipeline InsertBefore<TBefore>(ITemplateProcessor processor) where TBefore : ITemplateProcessor
{
int index = _processors.FindIndex(p => p is TBefore);
if (index >= 0)
_processors.Insert(index, processor);
else
_processors.Add(processor);
return this;
}
/// <summary>
/// Processes the document through all processors in the pipeline.
/// </summary>
public ProcessorResult ProcessAll(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options)
{
int totalReplacements = 0;
HashSet<string> allMissingVariables = new();
foreach (var processor in _processors)
{
var result = processor.Process(document, context, options);
if (!result.Success)
return result; // Propagate failure
totalReplacements += result.ReplacementCount;
foreach (var missing in result.MissingVariables)
allMissingVariables.Add(missing);
if (!result.ContinueProcessing)
break; // Stop pipeline
}
return ProcessorResult.Ok(totalReplacements, allMissingVariables.ToList());
}
}ContextAwareProcessor (wraps visitor pattern from Phase 2):
public sealed class ContextAwareProcessor : ITemplateProcessor
{
public string Name => "ContextAware";
public ProcessorResult Process(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options)
{
var walker = new DocumentWalker();
var visitor = new CompositeVisitor(
new ConditionalVisitor(),
new LoopVisitor(walker),
new PlaceholderVisitor()
);
walker.Walk(document.MainDocumentPart.Document.Body.Elements(), visitor, context);
// Return result with replacement count
return ProcessorResult.Ok(visitor.ReplacementCount, visitor.MissingVariables);
}
}ValidationProcessor (example custom processor):
public sealed class ValidationProcessor : ITemplateProcessor
{
public string Name => "Validation";
public ProcessorResult Process(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options)
{
// Validate template structure
// Check for malformed placeholders
// Check for unclosed blocks
if (hasErrors)
return ProcessorResult.Failure("Template validation failed: " + errors);
return ProcessorResult.Ok();
}
}New Public API (additive, non-breaking):
public sealed class DocumentTemplateProcessor
{
private TemplateProcessorPipeline? _customPipeline;
private PlaceholderReplacementOptions _options;
// Existing constructor
public DocumentTemplateProcessor(PlaceholderReplacementOptions? options = null)
{
_options = options ?? new PlaceholderReplacementOptions();
}
// NEW: Fluent configuration
public DocumentTemplateProcessor WithOptions(PlaceholderReplacementOptions options)
{
_options = options;
return this;
}
public DocumentTemplateProcessor UseProcessingMode(ProcessingMode mode)
{
_options = _options with { ProcessingMode = mode };
return this;
}
public DocumentTemplateProcessor UseMissingVariableBehavior(MissingVariableBehavior behavior)
{
_options = _options with { MissingVariableBehavior = behavior };
return this;
}
public DocumentTemplateProcessor UseCulture(CultureInfo culture)
{
_options = _options with { Culture = culture };
return this;
}
// NEW: Custom pipeline
public DocumentTemplateProcessor UseCustomPipeline(TemplateProcessorPipeline pipeline)
{
_customPipeline = pipeline;
return this;
}
public DocumentTemplateProcessor AddProcessor(ITemplateProcessor processor)
{
_customPipeline ??= CreateDefaultPipeline();
_customPipeline.AddProcessor(processor);
return this;
}
// Existing ProcessTemplate method remains unchanged for backward compatibility
}Example Usage:
// Simple usage (backward compatible)
var processor = new DocumentTemplateProcessor();
processor.ProcessTemplate(template, output, data);
// Fluent configuration
var processor = new DocumentTemplateProcessor()
.UseProcessingMode(ProcessingMode.ContextAware)
.UseMissingVariableBehavior(MissingVariableBehavior.ThrowException)
.UseCulture(CultureInfo.InvariantCulture);
// Custom pipeline
var processor = new DocumentTemplateProcessor()
.AddProcessor(new ValidationProcessor())
.AddProcessor(new ContextAwareProcessor())
.AddProcessor(new CustomFunctionProcessor());
// Advanced: Complete custom pipeline
var pipeline = new TemplateProcessorPipeline()
.AddProcessor(new ValidationProcessor())
.AddProcessor(new ConditionalProcessor())
.AddProcessor(new LoopProcessor())
.AddProcessor(new PlaceholderProcessor())
.AddProcessor(new AuditLogProcessor());
var processor = new DocumentTemplateProcessor()
.UseCustomPipeline(pipeline);FunctionProcessor (enables custom functions in expressions):
public sealed class FunctionProcessor : ITemplateProcessor
{
private readonly Dictionary<string, Func<object[], object>> _functions = new();
public FunctionProcessor RegisterFunction(string name, Func<object[], object> func)
{
_functions[name] = func;
return this;
}
public ProcessorResult Process(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options)
{
// Find and replace function calls like {{FormatDate(OrderDate, "yyyy-MM-dd")}}
// ...
}
}
// Usage:
processor.AddProcessor(
new FunctionProcessor()
.RegisterFunction("FormatDate", args => ((DateTime)args[0]).ToString((string)args[1]))
.RegisterFunction("Upper", args => args[0]?.ToString()?.ToUpper())
);HeaderFooterProcessor (process headers/footers):
public sealed class HeaderFooterProcessor : ITemplateProcessor
{
public string Name => "HeaderFooter";
public ProcessorResult Process(
WordprocessingDocument document,
IEvaluationContext context,
PlaceholderReplacementOptions options)
{
// Process placeholders in headers
foreach (var header in document.MainDocumentPart.HeaderParts)
{
// Process header content
}
// Process placeholders in footers
foreach (var footer in document.MainDocumentPart.FooterParts)
{
// Process footer content
}
return ProcessorResult.Ok();
}
}- Day 1-2: Create ITemplateProcessor and TemplateProcessorPipeline
- Day 3-4: Wrap Phase 2 visitors in ContextAwareProcessor
- Day 5: Implement fluent API on DocumentTemplateProcessor
- Day 1-2: Create example custom processors (Validation, Function, HeaderFooter)
- Day 3: Comprehensive testing of custom pipeline scenarios
- Day 4: Performance optimization and benchmarking
- Day 5: Final documentation, examples, and migration guide
[Fact]
public void ProcessTemplate_CustomProcessor_IsExecuted()
{
var customProcessor = new Mock<ITemplateProcessor>();
customProcessor.Setup(p => p.Process(It.IsAny<WordprocessingDocument>(),
It.IsAny<IEvaluationContext>(),
It.IsAny<PlaceholderReplacementOptions>()))
.Returns(ProcessorResult.Ok());
var processor = new DocumentTemplateProcessor()
.AddProcessor(customProcessor.Object);
processor.ProcessTemplate(template, output, data);
customProcessor.Verify(p => p.Process(It.IsAny<WordprocessingDocument>(),
It.IsAny<IEvaluationContext>(),
It.IsAny<PlaceholderReplacementOptions>()),
Times.Once);
}[Fact]
public void Pipeline_ExecutesProcessorsInOrder()
{
var executionOrder = new List<string>();
var processor1 = CreateMockProcessor("First", executionOrder);
var processor2 = CreateMockProcessor("Second", executionOrder);
var processor3 = CreateMockProcessor("Third", executionOrder);
var pipeline = new TemplateProcessorPipeline()
.AddProcessor(processor1)
.AddProcessor(processor2)
.AddProcessor(processor3);
pipeline.ProcessAll(document, context, options);
Assert.Equal(new[] { "First", "Second", "Third" }, executionOrder);
}- ✅ Backward compatible public API (existing code works unchanged)
- ✅ Fluent API enables easy configuration
- ✅ Custom processors can be added
- ✅ Pipeline order is configurable
- ✅ Example processors demonstrate extensibility
- ✅ All tests pass (Legacy and ContextAware modes)
- ✅ Performance within 10% of Phase 1 baseline
/\
/ \
/ E2E \ 5% - Full integration tests
/______\
/ \
/ Integ. \ 25% - Feature integration tests
/____________\
/ \
/ Unit Tests \ 70% - Unit tests for components
/__________________\
Unit Tests (~20 new tests):
- IEvaluationContext implementations
- GlobalEvaluationContext.TryResolveVariable
- LoopEvaluationContext with parent chaining
- Edge cases (null values, missing variables)
- ConditionalEvaluator with IEvaluationContext
- All operators still work
- Context resolution correct
Integration Tests (~5 new tests):
- Conditional inside simple loop
- Conditional with loop metadata (@index, @first, @last)
- Nested loops with conditionals
- Conditional in loop with complex expressions
- Bullet list with conditional (from ListsInLoopsTests)
Regression Tests:
- All 187 existing tests must pass
- No behavioral changes to top-level conditionals
- No behavioral changes to loops without conditionals
Unit Tests (~30 new tests):
- DocumentWalker element detection
- ConditionalVisitor logic
- LoopVisitor cloning and context creation
- PlaceholderVisitor replacement
- CompositeVisitor delegation
Integration Tests (~10 new tests):
- ContextAware mode vs Legacy mode comparison
- Complex nested structures in ContextAware mode
- Performance comparison tests
- Mixed conditionals/loops/tables in ContextAware mode
Parameterized Tests:
[Theory]
[InlineData(ProcessingMode.Legacy)]
[InlineData(ProcessingMode.ContextAware)]
public void AllFeatures_WorkInBothModes(ProcessingMode mode)
{
// Run same test in both modes
// ContextAware should pass, Legacy may skip/fail for conditionals in loops
}Unit Tests (~15 new tests):
- ITemplateProcessor interface
- TemplateProcessorPipeline composition
- Processor execution order
- ProcessorResult aggregation
Integration Tests (~8 new tests):
- Custom processor execution
- Pipeline ordering
- Fluent API configuration
- Example processors (Validation, Function, HeaderFooter)
End-to-End Tests (~3 new tests):
- Complete real-world scenarios with custom pipeline
- Performance stress tests
- Complex document with all features
| Phase | Line Coverage | Branch Coverage | Mutation Score |
|---|---|---|---|
| Phase 1 | > 85% | > 80% | > 75% |
| Phase 2 | > 88% | > 83% | > 78% |
| Phase 3 | > 90% | > 85% | > 80% |
Baseline (before refactoring):
Document Size: 50 pages, 200 paragraphs, 10 tables, 500 placeholders
Processing Time: ~150ms
Memory: ~20MB
Acceptable Impact:
- Phase 1: < 5% slowdown (< 158ms)
- Phase 2: < 5% additional (< 165ms)
- Phase 3: < 10% from baseline (< 165ms)
Benchmark Suite:
[Benchmark]
public void ProcessSimpleDocument() { /* 10 pages, 50 placeholders */ }
[Benchmark]
public void ProcessComplexDocument() { /* 50 pages, 500 placeholders, nested structures */ }
[Benchmark]
public void ProcessWithLoops() { /* 10 loops, 20 iterations each */ }
[Benchmark]
public void ProcessWithConditionals() { /* 50 conditionals */ }
[Benchmark]
public void ProcessMixedFeatures() { /* All features combined */ }No migration needed - All changes are internal and backward compatible.
Optional: Start preparing test data for conditionals in loops (will work after Phase 1).
Option 1: Stay on Legacy Mode (no changes required)
// Existing code works unchanged
var processor = new DocumentTemplateProcessor();
processor.ProcessTemplate(template, output, data);
// Behind the scenes: Uses ProcessingMode.Legacy (default)Option 2: Opt-in to ContextAware Mode (recommended for new templates)
var options = new PlaceholderReplacementOptions
{
ProcessingMode = ProcessingMode.ContextAware // Enable new behavior
};
var processor = new DocumentTemplateProcessor(options);
processor.ProcessTemplate(template, output, data);Breaking Changes: None (new option is additive)
Templates that benefit from ContextAware mode:
- ✅ Templates with conditionals inside loops
- ✅ Templates with complex nested structures
- ✅ Templates requiring loop variable access in conditionals
Templates that should stay on Legacy mode:
⚠️ Templates relying on conditionals evaluating globally before loops⚠️ Templates with performance-critical requirements (until benchmarks confirm)
Option 1: Continue using existing API (no changes)
var processor = new DocumentTemplateProcessor(options);
processor.ProcessTemplate(template, output, data);Option 2: Use fluent API (recommended for better readability)
var processor = new DocumentTemplateProcessor()
.UseProcessingMode(ProcessingMode.ContextAware)
.UseMissingVariableBehavior(MissingVariableBehavior.ThrowException)
.UseCulture(CultureInfo.InvariantCulture);
processor.ProcessTemplate(template, output, data);Option 3: Add custom processors
var processor = new DocumentTemplateProcessor()
.UseProcessingMode(ProcessingMode.ContextAware)
.AddProcessor(new ValidationProcessor())
.AddProcessor(new HeaderFooterProcessor());
processor.ProcessTemplate(template, output, data);Option 4: Use completely custom pipeline (advanced)
var pipeline = new TemplateProcessorPipeline()
.AddProcessor(new ValidationProcessor())
.AddProcessor(new ContextAwareProcessor())
.AddProcessor(new CustomFunctionProcessor())
.AddProcessor(new AuditLogProcessor());
var processor = new DocumentTemplateProcessor()
.UseCustomPipeline(pipeline);
processor.ProcessTemplate(template, output, data);Phase 2:
⚠️ ProcessingMode.Legacy- Marked as deprecated, recommend migrating to ContextAware- Still supported, will be removed in v2.0.0 (12 months notice)
Phase 3:
- None (all additions are new features)
| Version | Supported Features |
|---|---|
| v1.0.x | Current implementation (conditionals before loops) |
| v1.1.0 | Phase 1: IEvaluationContext, conditionals in loops |
| v1.2.0 | Phase 2: Visitor pattern, ContextAware mode |
| v1.3.0 | Phase 3: Extensible pipeline, fluent API |
| v2.0.0 | Future: Remove Legacy mode, breaking changes |
Functionality:
- ✅ Conditionals inside loops work correctly
- ✅ All existing tests pass (187 tests)
- ✅ 5+ new integration tests pass
Performance:
- ✅ Processing time increase < 5% (< 158ms baseline 150ms)
- ✅ Memory usage increase < 10% (< 22MB baseline 20MB)
Code Quality:
- ✅ Test coverage maintained > 85%
- ✅ No new code duplication introduced
- ✅ All existing functionality preserved
Documentation:
- ✅ README.md updated (limitation removed)
- ✅ Examples.md updated with conditionals in loops examples
- ✅ TODO.md updated (mark limitation as resolved)
- ✅ REFACTORING.md created (this document)
Functionality:
- ✅ Visitor pattern works for all features (went beyond plan - removed legacy path)
- ✅ All 319 tests pass (100% success rate)
- ✅ Table row loops work correctly
- ✅ Nested loops work correctly
- ✅ Conditionals inside loops work correctly
Code Quality:
- ✅ Code duplication eliminated (~300 lines removed)
- ✅ Clean architecture with visitor pattern implemented
- ✅ Test coverage 100% (all tests passing)
- ✅ Cyclomatic complexity reduced significantly
- ✅ Single processing path (no dual-mode complexity)
Performance:
- ⏸️ Formal benchmarking deferred (all tests pass with good performance)
- ✅ No performance regressions observed in test execution
Documentation:
- ✅ REFACTORING.md updated with actual implementation
- 🚧 ARCHITECTURE.md needs updating (in progress)
- ⏸️ Migration guide not needed (no dual-mode support)
Functionality:
- ✅ Custom processors can be added
- ✅ Pipeline order is configurable
- ✅ Fluent API works as designed
- ✅ Example processors demonstrate extensibility
Code Quality:
- ✅ ITemplateProcessor abstraction implemented
- ✅ TemplateProcessorPipeline composable
- ✅ Test coverage > 90%
- ✅ All SOLID principles followed
Performance:
- ✅ Within 10% of baseline (< 165ms)
- ✅ Custom processors have minimal overhead
Documentation:
- ✅ Extension guide with examples
- ✅ Custom processor tutorial
- ✅ API reference complete
- ✅ Real-world examples
By end of 5 weeks:
- ✅ Conditionals in loops working
- ✅ Code duplication eliminated
- ✅ Clean, extensible architecture
- ✅ No regressions in existing functionality
- ✅ Performance within acceptable range
- ✅ Comprehensive test coverage (> 90%)
- ✅ Full documentation
- ✅ Example custom processors
- ✅ Migration path clear
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Phase 1 | |||
| Context chaining breaks nested loops | Medium | High | Comprehensive unit tests for LoopContext |
| Conditional evaluation wrong in loop scope | Medium | High | Test all operators with loop variables |
| Performance degrades > 5% | Low | Medium | Benchmark early, optimize if needed |
| Phase 2 | |||
| Visitor pattern too complex | Medium | Low | Clear documentation, code examples |
| Legacy mode diverges from ContextAware | Medium | High | Share as much code as possible |
| DocumentWalker infinite loop on complex docs | Low | High | Cycle detection, max depth limit |
| Phase 3 | |||
| Custom processors break core functionality | Medium | High | Sandboxing, validation, safe defaults |
| Pipeline overhead too high | Low | Medium | Lazy initialization, processor pooling |
| API too complex for users | Medium | Low | Simple defaults, good examples |
| Overall | |||
| Timeline slips | High | Medium | Phased approach allows stopping at any phase |
| Team loses focus | Medium | Low | Clear phase deliverables, regular demos |
| Breaking changes upset users | Low | Low | Not in production, versioning strategy |
If Phase 1 takes longer than 1 week:
- Acceptable: Conditionals in loops is high priority
- Consider pair programming to unblock
- May delay Phase 2 start
If Phase 2 complexity is too high:
- Can ship Phase 1 and reassess
- Consider simpler alternative to visitor pattern
- May split into 2 phases
If Phase 3 timeline slips:
- Phase 3 is nice-to-have, can defer
- Can release Phase 1+2 as v1.1.0
- Phase 3 becomes v1.2.0 or v1.3.0
If performance issues arise:
- Profile and optimize hot paths
- Consider caching evaluation results
- May need to revisit architecture decisions
- Acceptable to take more time for optimization
TriasDev.Templify/
├── DocumentTemplateProcessor.cs (Main entry point)
├── PlaceholderReplacementOptions.cs
├── ProcessingResult.cs
│
├── Conditionals/
│ ├── ConditionalProcessor.cs
│ ├── ConditionalDetector.cs
│ ├── ConditionalBlock.cs
│ └── ConditionalEvaluator.cs
│
├── Loops/
│ ├── LoopProcessor.cs
│ ├── LoopDetector.cs
│ ├── LoopBlock.cs
│ └── LoopContext.cs
│
├── Placeholders/
│ ├── DocumentBodyReplacer.cs
│ ├── TableReplacer.cs
│ ├── PlaceholderMatch.cs
│ └── ValueResolver.cs
│
└── Utilities/
├── PropertyPathResolver.cs
└── TextElementFormatter.cs
TriasDev.Templify/
├── Abstractions/
│ ├── IEvaluationContext.cs (NEW)
│ ├── GlobalEvaluationContext.cs (NEW)
│ └── LoopEvaluationContext.cs (NEW)
TriasDev.Templify/
├── Visitors/
│ ├── ITemplateElementVisitor.cs (NEW)
│ ├── DocumentWalker.cs (NEW)
│ ├── ConditionalVisitor.cs (NEW)
│ ├── LoopVisitor.cs (NEW)
│ ├── PlaceholderVisitor.cs (NEW)
│ └── CompositeVisitor.cs (NEW)
│
└── PlaceholderReplacementOptions.cs (MODIFIED)
└── ProcessingMode enum (NEW)
TriasDev.Templify/
├── Pipeline/
│ ├── ITemplateProcessor.cs (NEW)
│ ├── TemplateProcessorPipeline.cs (NEW)
│ ├── ProcessorResult.cs (NEW)
│ └── ContextAwareProcessor.cs (NEW)
│
├── Examples/
│ ├── ValidationProcessor.cs (NEW)
│ ├── FunctionProcessor.cs (NEW)
│ └── HeaderFooterProcessor.cs (NEW)
│
└── DocumentTemplateProcessor.cs (MODIFIED)
└── Fluent API methods (NEW)
- Visitor Pattern - Gang of Four
- Strategy Pattern - Gang of Four
- Chain of Responsibility - Gang of Four
- OpenXML SDK Documentation
| Date | Version | Changes |
|---|---|---|
| 2025-01-08 | 1.0 | Initial refactoring plan created |
Document Owner: Development Team Last Updated: 2025-01-08 Next Review: After each phase completion