Blazor Developer Tools v0.10: A Deep Dive into Framework-Level Integration
Source: Dev.to
Introduction
When I first released Blazor Developer Tools, my goal was to give Blazor developers the same component‑inspection experience that React developers have enjoyed for years. React DevTools lets you see your component tree, inspect props in real‑time, and understand your application’s structure. Blazor had nothing equivalent.
The v0.9 release was a working MVP, but its architecture had fundamental limitations that would eventually hit a wall. After weeks of studying the Blazor source code and exploring dead ends, I arrived at a completely new architecture for v0.10 that solves these problems at the framework level.
This post explains what I learned, the options I considered, and why the new approach works.
v0.9 Architecture
The v0.9 architecture was clever but ultimately a workaround. It consisted of three steps:
- Build‑time transformation – An MSBuild task scanned your
.razorfiles, created shadow copies, and injected invisible<span>markers with data attributes containing component metadata. - Runtime detection – The browser extension scanned the DOM for these markers.
- Tree reconstruction – The extension rebuilt the component hierarchy from the marker positions.
<!-- injected marker example (empty in original) -->
Problems with v0.9
-
Component‑library conflicts – Some libraries (e.g., MudBlazor) validate their children and throw exceptions when unexpected elements appear. The injected spans caused such failures:
<!-- example of problematic injected markup --> <span data-bdt-component-id="123" style="display:none;"></span> -
Work‑around required – I added a
SkipComponentsconfiguration so users could exclude problematic components, but this was a patch, not a solution. Users had to discover which components broke and configure around them. -
Static metadata – The span markers contained metadata captured at build time, with no connection to the actual running component instances. Consequently:
- No live parameter values.
- No performance metrics (render counts, timing).
- No component state – the markers were dead HTML, disconnected from the living Blazor application.
-
DOM pollution – Injecting invisible elements felt wrong. It could affect CSS selectors, automated tests, or edge cases.
Desired Capabilities
At its heart, the new architecture needed to answer two questions for any DOM element:
- Which component rendered this element?
- What are the current parameter values for that component?
Blazor does not expose public APIs to answer either question. The component tree lives in .NET memory, the DOM lives in the browser, and there is no public bridge between them.
Blazor Rendering Pipeline
A simplified view of the rendering flow:
┌─────────────────────────────────────────────────────────────────┐
│ .NET SIDE │
│ │
│ Component Instance │
│ │ │
│ ▼ │
│ BuildRenderTree() → RenderTreeFrames │
│ │ │
│ ▼ │
│ Renderer assigns componentId, diffs against previous tree │
│ │ │
│ ▼ │
│ RenderBatch (binary format) │
│ │
└─────────────────────────────────────────────────────────────────┘
│
│ SignalR (Server) or
│ Direct call (WebAssembly)
▼
┌─────────────────────────────────────────────────────────────────┐
│ BROWSER SIDE │
│ │
│ blazor.server.js / blazor.webassembly.js │
│ │ │
│ ▼ │
│ BrowserRenderer interprets RenderBatch │
│ │ │
│ ▼ │
│ DOM mutations applied │
│ │
└─────────────────────────────────────────────────────────────────┘
Key insight: Blazor maintains an internal mapping between componentId (an integer assigned to each component instance) and its DOM location. This mapping exists in the JavaScript runtime’s BrowserRenderer, but it is private. If I could intercept component creation and correlate it with what JavaScript sees, I would have the bridge I need.
Approaches That Didn’t Work
1. Piggyback on CSS Isolation
Blazor already adds empty b-xxxxxxxxxx attributes to elements for scoped CSS. I wondered whether I could reuse this mechanism.
- Problem: The Razor compilation process is a black box. The CSS‑isolation attributes are added by the compiler itself, not via an extensible pipeline, so there is no hook to inject additional attributes.
2. Require a Custom Base Class
Force all components to inherit from BdtComponentBase instead of ComponentBase.
public class Counter : BdtComponentBase // instead of ComponentBase
{
// …
}
- User friction: Requiring changes to every component is a non‑starter.
- Inheritance conflicts: Many projects already use custom base classes or library‑provided bases.
- Third‑party components: You cannot modify MudBlazor, Radzen, or other external components.
3. Source Generator to Inject Registration Code
Generate code that registers each component during its lifecycle, e.g., in OnInitialized.
// Generated code injected into component
protected override void OnInitialized()
{
BdtRegistry.Register(this);
base.OnInitialized();
}
- Existing overrides: If the user already overrides
OnInitialized, we would have a conflict. Wrapping theirs raises questions about call order. - Partial‑class complexity: Razor components are already partial classes with generated code; adding more generated lifecycle overrides creates fragile interactions.
- Edge cases: Components that override
SetParametersAsyncwithout callingbase, or components that do not inherit fromComponentBaseat all, break this approach.
4. Modified Blazor SDK (The “Nuclear” Option)
Create a custom Blazor SDK that includes tracking instrumentation.
- Maintenance burden: Every new Blazor release would require merging changes into the custom SDK.
- User friction: Developers would need to reference a non‑standard SDK.
- Trust concerns: Replacing core framework components is a hard sell for most teams.
Conclusion
The v0.9 architecture demonstrated that component‑inspection is possible, but injecting static markers into the DOM is fragile and limited. By understanding Blazor’s internal componentId mapping and intercepting component creation at the framework level, v0.10 establishes a robust, live bridge between .NET component instances and their rendered DOM elements—enabling real‑time inspection of parameters, state, and performance metrics without polluting the DOM or requiring invasive changes to user code.