Skip to content

[Refactor]: Change translation pipeline to middleware pattern #217

@ParadiseFallen

Description

@ParadiseFallen

Enhance VDOM Translator Architecture with Middleware Support

Problem

The current VDOM translator architecture lacks extensibility for cross-cutting concerns. Specific issues:

Debug Translation Issues

Concrete problem: When debugging why a custom translator isn't being called, there's no visibility into the translation process.

Example scenario:

// Developer creates a custom translator
public class MyCustomTranslator : IVdomElementTranslator
{
    public int Priority => 50;
    public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
    {
        if (node.TagName == "my-custom-tag")
        {
            renderable = new Markup("Custom content");
            return true;
        }
        renderable = null;
        return false;
    }
}

Current debugging process:

  1. Translator isn't called — no way to know why
  2. Must add Console.WriteLine($"Trying {node.TagName}") to every translator (20+ files)
  3. Or manually check priorities: "Is my Priority 50 before or after the translator that's handling it?"
  4. Or set breakpoints in all translators and step through

With middleware - how it changes:

1. Translator isn't called - now you can see why:

// Add ONE debug middleware (not 20+ translators)
public class DebugMiddleware : IVdomTranslationMiddleware
{
    public IRenderable? Translate(VNode node, TranslationContext context, 
        TranslationDelegate next)
    {
        Console.WriteLine($"[DEBUG] Translating node: {node.TagName}, attributes: {string.Join(", ", node.Attributes.Keys)}");
        var renderable = next(node, context);
        Console.WriteLine($"[DEBUG] Result: {(renderable != null ? "SUCCESS" : "FAILED")}");
        return renderable;
    }
}
// Output shows: "[DEBUG] Translating node: div, attributes: class"
//               "[DEBUG] Result: SUCCESS"
// Now you know: your translator wasn't called because node.TagName was 'div', not 'my-custom-tag'

2. No need to modify every translator:

  • Before: Modify 20+ translator files, add Console.WriteLine to each
  • After: Add ONE middleware, register it once
// Before: Modify 20+ files
// PanelElementTranslator.cs: Console.WriteLine(...)
// GridElementTranslator.cs: Console.WriteLine(...)
// ButtonElementTranslator.cs: Console.WriteLine(...)
// ... 17 more files

// After: One middleware file
services.AddVdomMiddleware<DebugMiddleware>();

3. Track translation attempts and results:

public class DebugMiddleware : IVdomTranslationMiddleware
{
    private readonly List<TranslationAttempt> _attempts = new();
    
    public IRenderable? Translate(VNode node, TranslationContext context, 
        TranslationDelegate next)
    {
        var startTime = Stopwatch.GetTimestamp();
        var renderable = next(node, context);
        var elapsed = Stopwatch.GetElapsedTime(startTime);
        
        // Record attempt with node info and result
        _attempts.Add(new TranslationAttempt
        {
            TagName = node.TagName,
            Attributes = string.Join(", ", node.Attributes.Keys),
            Success = renderable != null,
            ElapsedMs = elapsed.TotalMilliseconds
        });
        
        Console.WriteLine($"[DEBUG] Node: {node.TagName}, Result: {(renderable != null ? "SUCCESS" : "FAILED")}, Time: {elapsed.TotalMilliseconds:F2}ms");
        return renderable;
    }
    
    // Query later: "Which nodes failed translation?"
    public IEnumerable<TranslationAttempt> GetFailedAttempts() => 
        _attempts.Where(a => !a.Success);
}
// Output: "[DEBUG] Node: div, Result: SUCCESS, Time: 2.34ms"
//         "[DEBUG] Node: my-custom-tag, Result: FAILED, Time: 0.12ms"
// Now you know: 'my-custom-tag' failed translation, took 0.12ms

4. Single breakpoint instead of 20+:

// Before: Set breakpoints in 20+ translator files
// PanelElementTranslator.cs: breakpoint
// GridElementTranslator.cs: breakpoint
// ... 18 more breakpoints

// After: One breakpoint in middleware
public class DebugMiddleware : IVdomTranslationMiddleware
{
    public IRenderable? Translate(VNode node, TranslationContext context, 
        TranslationDelegate next)
    {
        // Set ONE breakpoint here - see all translation attempts
        var nodeInfo = $"{node.TagName} with {node.Attributes.Count} attributes";
        var renderable = next(node, context);
        // Inspect: nodeInfo, renderable (null = not handled, non-null = handled)
        return renderable;
    }
}

Add Observability

Concrete problem: No infrastructure exists to measure translation performance or collect statistics.

Current situation:

  • Application feels slow, suspect translation pipeline
  • No way to measure: "Is translation taking 10ms or 100ms per node?"
  • No way to identify: "Which translator is slow? PanelElementTranslator? GridElementTranslator?"
  • No statistics: "What's the most common node type? What's the success rate?"

To measure currently:

// Must manually instrument EVERY translator
public class PanelElementTranslator : IVdomElementTranslator
{
    public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
    {
        var sw = Stopwatch.StartNew(); // ❌ Manual instrumentation
        // ... translation code ...
        sw.Stop();
        Console.WriteLine($"PanelElementTranslator took {sw.ElapsedMilliseconds}ms"); // ❌ Manual logging
        return true;
    }
}
// Repeat for 20+ translators

With middleware (infrastructure provided):

Option 1: Measure entire pipeline (simple)

// Measures total translation time for the node
public class PerformanceMiddleware : IVdomTranslationMiddleware
{
    public IRenderable? Translate(VNode node, TranslationContext context, 
        TranslationDelegate next)
    {
        var sw = Stopwatch.StartNew();
        var renderable = next(node, context);
        sw.Stop();
        
        _metrics.RecordTotalTime(node.TagName, sw.ElapsedMilliseconds, renderable != null);
        return renderable;
    }
}
// Answer: "Translation of 'div' nodes takes 15ms total"

Note:

  • Middleware measures the entire pipeline (all translators tried until one succeeds)
  • For per-translator metrics, you'd need to wrap each translator individually (requires factory/registration changes)
  • Simple middleware approach measures total time, which is sufficient for most profiling needs
  • Middleware infrastructure will be provided, but developers write their own middleware for specific needs

Handle Errors

Concrete problem: While stack traces show which translator threw, they don't include node context in the exception message.

Example scenario:

// Translator throws exception
public class PanelElementTranslator : IVdomElementTranslator
{
    public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
    {
        // ... code ...
        var width = int.Parse(node.Attributes["data-width"]); // ❌ Throws if missing
        // ...
    }
}

Current error:

System.FormatException: Input string was not in a correct format.
   at System.Int32.Parse(String s)
   at PanelElementTranslator.TryTranslate(...)  // ✅ Stack trace shows translator
   at VdomSpectreTranslator.TryTranslateElement(...)

Problems:

  • ❌ Exception message doesn't say which node caused it (<div class="panel" data-width="invalid">)
  • ❌ No information about node attributes in the exception
  • ❌ Must attach debugger or add try-catch with Console.WriteLine to see node details

With middleware (applies to ALL translators automatically):

// Add ONCE, applies to all 20+ translators automatically
public class ErrorContextMiddleware : IVdomTranslationMiddleware
{
    public IRenderable? Translate(VNode node, TranslationContext context, 
        TranslationDelegate next)
    {
        try
        {
            return next(node, context);
        }
        catch (Exception ex)
        {
            // Wrap with node context - applies to ANY translator that throws
            throw new TranslationException(
                $"Translation failed for node: {node.TagName}, attributes: {string.Join(", ", node.Attributes)}",
                ex);
        }
    }
}
// Register once:
services.AddVdomMiddleware<ErrorContextMiddleware>();
// Now ALL translator errors show: "Translation failed for node: div, attributes: class=panel, data-width=invalid"
// No need to modify each of 20+ translators individually

Modify Nodes

Cannot normalize attributes, validate nodes, or transform structure before translation.

Enhance Results

Cannot automatically wrap renderables, apply global styles, or cache results.

Current Limitations

1. No Observability

Current code:

// src/RazorConsole.Core/Vdom/VdomSpectreTranslator.cs:103-110
foreach (var translator in _elementTranslators)
{
    if (translator.TryTranslate(node, context, out var candidate) && candidate is not null)
    {
        renderable = candidate;
        return true;  // ❌ No record of which translator handled this
    }
}

Problems:

  • Cannot determine which translator handled a specific node
  • Cannot measure translation time per translator
  • Cannot track translation statistics
  • No way to identify performance bottlenecks

With middleware:

To track which translator handled a node, you can use a Factory pattern to optionally wrap each translator with instrumentation.

Note: This wraps IVdomElementTranslator (translators), not IVdomTranslationMiddleware (middleware).

  • Translators (IVdomElementTranslator) use Priority and TryTranslate() — they remain unchanged
  • Middleware (IVdomTranslationMiddleware) use Translate() without Priority — this is the new architecture
// Factory interface for creating wrapped translators
public interface IVdomTranslatorFactory
{
    IVdomElementTranslator Create(IVdomElementTranslator translator);
}

// Default factory - returns translator as-is
public class DefaultTranslatorFactory : IVdomTranslatorFactory
{
    public IVdomElementTranslator Create(IVdomElementTranslator translator) => translator;
}

// Factory that wraps translators with performance tracking
public class InstrumentedTranslatorFactory : IVdomTranslatorFactory
{
    private readonly IMetricsCollector _metrics;
    
    public InstrumentedTranslatorFactory(IMetricsCollector metrics)
    {
        _metrics = metrics;
    }
    
    public IVdomElementTranslator Create(IVdomElementTranslator translator)
    {
        return new InstrumentedTranslator(translator, _metrics);
    }
}

// Wrapper that instruments translator calls
// Note: This wraps IVdomElementTranslator (which has Priority and TryTranslate)
// Middleware uses Translate() without Priority
public class InstrumentedTranslator : IVdomElementTranslator
{
    private readonly IVdomElementTranslator _inner;
    private readonly IMetricsCollector _metrics;
    
    public InstrumentedTranslator(IVdomElementTranslator inner, IMetricsCollector metrics)
    {
        _inner = inner;
        _metrics = metrics;
    }
    
    // Translators still have Priority (legacy, not used for ordering in new architecture)
    public int Priority => _inner.Priority;
    
    // Translators still use TryTranslate (translator interface)
    public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
    {
        var sw = Stopwatch.StartNew();
        var result = _inner.TryTranslate(node, context, out renderable);
        sw.Stop();
        
        _metrics.RecordTranslatorTime(_inner.GetType().Name, sw.ElapsedMilliseconds, result);
        return result;
    }
}

Registration with DI:

// Step 1: Register factory (optional - defaults to DefaultTranslatorFactory if not registered)
services.AddSingleton<IVdomTranslatorFactory, DefaultTranslatorFactory>();
// Or use instrumented factory:
// services.AddSingleton<IVdomTranslatorFactory>(sp => 
//     new InstrumentedTranslatorFactory(sp.GetRequiredService<IMetricsCollector>()));

// Step 2: Register translators directly in DI (normal DI registration)
services.AddDefaultVdomTranslators();  // Built-in translators registered as IVdomElementTranslator
services.AddVdomTranslator<MyCustomTranslator>();  // Custom translator registered as IVdomElementTranslator

// Step 3: VdomSpectreTranslator registration - factory is called HERE
services.AddSingleton<VdomSpectreTranslator>(sp =>
{
    // Get factory from DI (must be registered, or use Replace service to set default)
    var factory = sp.GetRequiredService<IVdomTranslatorFactory>();
    
    // Get all registered translators from DI
    var rawTranslators = sp.GetServices<IVdomElementTranslator>();  // All translators from DI
    
    // Factory is called here to wrap each translator
    var wrappedTranslators = rawTranslators
        .Select(t => factory.Create(t))  // ← Factory.Create() called for each translator
        .ToList();
    
    return new VdomSpectreTranslator(wrappedTranslators);
});

How it works:

  1. Translators registered in DI — All translators (built-in and custom) are registered as IVdomElementTranslator in DI container
  2. Factory registered separately — Factory must be registered as IVdomTranslatorFactory (use Replace service if you want to override default)
  3. Factory called during VdomSpectreTranslator creation — When DI creates VdomSpectreTranslator:
    • Resolves all IVdomElementTranslator instances from DI
    • Resolves IVdomTranslatorFactory from DI (required)
    • Calls factory.Create(translator) for each translator ← Factory is invoked here
    • Factory can return translator as-is (DefaultTranslatorFactory) or wrap it (InstrumentedTranslatorFactory)
    • Wrapped translators are passed to VdomSpectreTranslator constructor

Reference: Factory Method Pattern

2. No Pre-Processing

Cannot normalize attributes, validate nodes, or transform structure before translation.

3. No Post-Processing

Cannot automatically wrap renderables, apply global styles, or cache results.

4. Poor Error Handling

Current code:

// src/RazorConsole.Core/Vdom/VdomSpectreTranslator.cs:113-115
catch (Exception)
{
    throw;  // ❌ Loses context: which translator? what node?
}

Problems:

  • Stack trace doesn't show which translator threw the exception
  • No information about the node being processed (tag name, attributes, etc.)
  • Difficult to diagnose issues in production
  • Cannot wrap exceptions with additional context

Real-World Impact

Scenario: Debugging why a custom translator isn't called

  • Current: Add Console.WriteLine to every translator, manually trace priorities
  • With middleware: Debug middleware tracks translation flow

Scenario: Performance profiling

  • Current: Cannot measure translation time or identify slow translators
  • With middleware: Built-in performance metrics

Scenario: Node normalization

  • Current: Must modify every translator or create wrapper components
  • With middleware: Single normalization middleware handles all cases

Scenario: Post-processing (wrapping results)

  • Current: Must modify each translator individually to wrap renderables
  • With middleware: Single wrapper middleware handles all cases

Proposed Solution

Refactor the translator architecture to use a middleware pipeline pattern:

flowchart TD
    VNode[VNode] --> ExtPoint[Extension point<br/>Middleware Pipeline]
    ExtPoint --> PreMW1[Pre-Processing<br/>Middleware 1]
    PreMW1 --> PreMW2[Pre-Processing<br/>Middleware 2]
    PreMW2 --> BuiltIn[Built in translators<br/>Translators]
    BuiltIn --> PostMW1[Post-Processing<br/>Middleware 1]
    PostMW1 --> PostMW2[Post-Processing<br/>Middleware 2]
    PostMW2 --> Renderable[IRenderable]
    
    style ExtPoint fill:#e1f5ff
    style BuiltIn fill:#d4edda
    style Renderable fill:#fff3cd
Loading

Middleware Interface

/// <summary>
/// Delegate representing the next middleware in the translation pipeline.
/// </summary>
public delegate IRenderable? TranslationDelegate(VNode node, TranslationContext context);

public interface IVdomTranslationMiddleware
{
    /// <summary>
    /// Processes a translation request, optionally calling the next middleware in the pipeline.
    /// </summary>
    /// <param name="node">The VNode to translate. Can be modified and passed to next() for pre-processing.</param>
    /// <param name="context">The translation context for recursive translation.</param>
    /// <param name="next">The next middleware in the pipeline.</param>
    /// <returns>The translated renderable, or null if translation failed.</returns>
    IRenderable? Translate(VNode node, TranslationContext context, TranslationDelegate next);
}

Key design decisions:

  • Synchronous — All translation is synchronous
  • No Priority — Order determined by registration order (simpler, more predictable)
  • Simple return value — Returns IRenderable? directly (null = not handled, non-null = handled)
  • Pre-processing — Middleware can modify node and pass it to next() before translation
  • Post-processing — Middleware can wrap/enhance renderable returned from next() after translation
  • Exception handling — Unhandled nodes throw exceptions

Registration

Translators (built-in and custom):

Translators are registered directly in DI as IVdomElementTranslator:

// Register built-in translators (automatically registered by default)
services.AddDefaultVdomTranslators();

// Register custom translator by type
services.AddVdomTranslator<MyCustomTranslator>();

// Register custom translator by instance
services.AddVdomTranslator(new MyCustomTranslator());

// Register custom translator with factory (for DI dependencies)
services.AddVdomTranslator(sp => 
    new MyCustomTranslator("custom str"));

Middleware:

// Register middleware (order matters - registered first = executed first)
services.AddVdomMiddleware<ErrorContextMiddleware>();
services.AddVdomMiddleware<PerformanceMiddleware>();
services.AddVdomMiddleware<DebugMiddleware>();

Translator Factory:

Factory is registered separately and is called during VdomSpectreTranslator creation:

// Default factory - returns translators as-is
services.AddSingleton<IVdomTranslatorFactory, DefaultTranslatorFactory>();

// Or replace with instrumented factory - wraps all translators with performance tracking
services.Replace(ServiceDescriptor.Singleton<IVdomTranslatorFactory>(sp => 
    new InstrumentedTranslatorFactory(sp.GetRequiredService<IMetricsCollector>())));

// Factory is invoked in VdomSpectreTranslator registration:
// services.AddSingleton<VdomSpectreTranslator>(sp => {
//     var factory = sp.GetRequiredService<IVdomTranslatorFactory>();  // Must be registered
//     var translators = sp.GetServices<IVdomElementTranslator>()
//         .Select(t => factory.Create(t))  // ← Factory called here
//         ...
// });

Benefits

  • Observability infrastructure — Middleware pipeline enables adding metrics/profiling middleware (developers write their own)
  • Flexibility — Pre/post processing through middleware pipeline
  • Better debugging — Middleware can capture and expose translation flow information
  • Extensibility — Easy to add cross-cutting concerns without modifying core translation logic

Success Criteria

  1. Can add debug/tracing middleware to the pipeline
  2. Can normalize/validate nodes before translation
  3. Can wrap/enhance renderables after translation
  4. Can profile translation performance
  5. Better error messages with translator and node context
  6. Middleware pipeline follows the chain of responsibility pattern

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions