-
Notifications
You must be signed in to change notification settings - Fork 41
Description
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:
- Translator isn't called — no way to know why
- Must add
Console.WriteLine($"Trying {node.TagName}")to every translator (20+ files) - Or manually check priorities: "Is my Priority 50 before or after the translator that's handling it?"
- 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.WriteLineto 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.12ms4. 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+ translatorsWith 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.WriteLineto 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 individuallyModify 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) usePriorityandTryTranslate()— they remain unchanged - Middleware (
IVdomTranslationMiddleware) useTranslate()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:
- Translators registered in DI — All translators (built-in and custom) are registered as
IVdomElementTranslatorin DI container - Factory registered separately — Factory must be registered as
IVdomTranslatorFactory(useReplaceservice if you want to override default) - Factory called during VdomSpectreTranslator creation — When DI creates
VdomSpectreTranslator:- Resolves all
IVdomElementTranslatorinstances from DI - Resolves
IVdomTranslatorFactoryfrom 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
VdomSpectreTranslatorconstructor
- Resolves all
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.WriteLineto 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
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
nodeand pass it tonext()before translation - Post-processing — Middleware can wrap/enhance
renderablereturned fromnext()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
- Can add debug/tracing middleware to the pipeline
- Can normalize/validate nodes before translation
- Can wrap/enhance renderables after translation
- Can profile translation performance
- Better error messages with translator and node context
- Middleware pipeline follows the chain of responsibility pattern
References
- Implementation:
src/RazorConsole.Core/Vdom/VdomSpectreTranslator.cs - Analysis:
design-doc/translator-middleware-analysis.md - Middleware Pattern: https://refactoring.guru/design-patterns/chain-of-responsibility
- Factory Method Pattern: https://refactoring.guru/design-patterns/factory-method