<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Joseph Gregory</title>
    <description>The latest articles on Forem by Joseph Gregory (@joseph_gregory_c4be294afc).</description>
    <link>https://forem.com/joseph_gregory_c4be294afc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3609034%2F482a7202-79a2-49e4-a1fc-0370996e4e2c.png</url>
      <title>Forem: Joseph Gregory</title>
      <link>https://forem.com/joseph_gregory_c4be294afc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/joseph_gregory_c4be294afc"/>
    <language>en</language>
    <item>
      <title>Blazor Developer Tools Beta 1: It Finally Talks to the Renderer</title>
      <dc:creator>Joseph Gregory</dc:creator>
      <pubDate>Thu, 26 Feb 2026 22:06:47 +0000</pubDate>
      <link>https://forem.com/joseph_gregory_c4be294afc/blazor-developer-tools-beta-1-it-finally-talks-to-the-renderer-ch2</link>
      <guid>https://forem.com/joseph_gregory_c4be294afc/blazor-developer-tools-beta-1-it-finally-talks-to-the-renderer-ch2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://github.com/joe-gregory/blazor-devtools" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://blazordevelopertools.com/" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt; | &lt;a href="https://www.nuget.org/packages/BlazorDeveloperTools/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; | &lt;a href="https://chromewebstore.google.com/detail/blazor-developer-tools/pfddbenemjnlceffaemllejnjbobadhp" rel="noopener noreferrer"&gt;Chrome Extension&lt;/a&gt; | &lt;a href="https://microsoftedge.microsoft.com/addons/detail/blazor-developer-tools/pdggeigaaicabckehkeldfpfikihgcdj" rel="noopener noreferrer"&gt;Edge Extension&lt;/a&gt; | &lt;a href="https://youtube.com/shorts/d1EFcvKYBbI" rel="noopener noreferrer"&gt;30-second demo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If you've ever used React DevTools, you know how valuable it is to see your component tree, understand why something re-rendered, and find performance bottlenecks. Blazor has never had anything like that. So I built it.&lt;/p&gt;

&lt;p&gt;Some of you may remember my earlier posts about Blazor Developer Tools. Those versions worked, but they were limited — I couldn't get the data I really needed without requiring developers to change their code. Beta 1 is a complete architectural rewrite, and it's the version I originally had in mind when I started this project.&lt;/p&gt;

&lt;p&gt;The difference: &lt;strong&gt;BDT now talks directly to the Blazor renderer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The Blazor renderer doesn't expose its component tree publicly. Earlier versions of BDT had to work around this, which meant limited visibility and more setup required from the developer.&lt;/p&gt;

&lt;p&gt;In beta 1, BDT uses reflection to sync with the renderer's internal state. That means it can now read component IDs, parent-child relationships, parameters, and internal state — all without you changing a single line of your component code.&lt;/p&gt;

&lt;p&gt;Install the NuGet package, register the service, and you immediately get a full component tree in your browser DevTools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorDeveloperTools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorDevTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Open DevTools, look for the "Blazor" tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Pillars Architecture
&lt;/h2&gt;

&lt;p&gt;Beta 1 is built on three pillars that work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillar 1: Component Activator&lt;/strong&gt; — automatically intercepts all component instantiation. Every component that gets created, BDT knows about it. No opt-in required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillar 2: Renderer Sync&lt;/strong&gt; — this is the new one. BDT reads the Blazor renderer through reflection to get the real component tree: IDs, hierarchy, parameters, state. This is what makes the component tree view and "why did this render?" diagnostics possible without any code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillar 3: Enhanced Components (opt-in)&lt;/strong&gt; — for developers who want the deepest metrics, inheriting from &lt;code&gt;BlazorDevToolsComponentBase&lt;/code&gt; gives you lifecycle method timing, ShouldRender tracking, StateHasChanged counts, and parameter change detection.&lt;/p&gt;

&lt;p&gt;Pillars 1 and 2 are fully automatic. Pillar 3 is there for when you need to dig deeper into a specific component.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get in Beta 1
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Component Tree&lt;/strong&gt; — your full Blazor component hierarchy, live in DevTools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeline Profiler&lt;/strong&gt; — hit record, interact with your app, see every render event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flamegraph&lt;/strong&gt; — visual swimlane timeline of component events with zoom and pan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why Did This Render?"&lt;/strong&gt; — click any render event to see what triggered it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranked View&lt;/strong&gt; — find your slowest and most frequently rendering components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element Picker&lt;/strong&gt; — click anything on the page to find its Blazor component&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Honest About the Current State
&lt;/h2&gt;

&lt;p&gt;This is a beta. There are rough edges. One feature isn't fully working yet. It currently supports InteractiveAuto (Server) render mode only — WebAssembly support is planned.&lt;/p&gt;

&lt;p&gt;But the core architecture is solid, and the foundation that previous versions were missing is now in place. This is the version I can actually build on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking for Testers and Contributors
&lt;/h2&gt;

&lt;p&gt;If you build with Blazor, I'd genuinely appreciate you trying it out and telling me what works and what doesn't. Open issues, start discussions, or just DM me.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐛 &lt;a href="https://github.com/joe-gregory/blazor-devtools/issues" rel="noopener noreferrer"&gt;Report Issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://github.com/joe-gregory/blazor-devtools/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://x.com/joegregorydev" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Blazor Developer Tools v0.10: A Deep Dive into Framework-Level Integration</title>
      <dc:creator>Joseph Gregory</dc:creator>
      <pubDate>Tue, 02 Dec 2025 04:07:43 +0000</pubDate>
      <link>https://forem.com/joseph_gregory_c4be294afc/blazor-developer-tools-v010-a-deep-dive-into-framework-level-integration-1b6m</link>
      <guid>https://forem.com/joseph_gregory_c4be294afc/blazor-developer-tools-v010-a-deep-dive-into-framework-level-integration-1b6m</guid>
      <description>&lt;p&gt;When I first released Blazor Developer Tools, I had a simple goal: 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.&lt;/p&gt;

&lt;p&gt;The v0.9 release was a working MVP, but I always knew the architecture had fundamental limitations that would eventually hit a wall. After weeks of studying the Blazor source code and exploring dead ends, I've arrived at a completely new architecture for v0.10 that solves these problems at the framework level.&lt;/p&gt;

&lt;p&gt;This post explains what I learned, the options I considered, and why the new approach works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Approach: Invisible Span Markers
&lt;/h2&gt;

&lt;p&gt;The v0.9 architecture was clever but ultimately a workaround. Here's how it worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build-time transformation&lt;/strong&gt;: An MSBuild task would scan your &lt;code&gt;.razor&lt;/code&gt; files, create shadow copies, inject invisible &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; markers with data attributes containing component metadata in the show files and feed the shadow files down the normal razor pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime detection&lt;/strong&gt;: The browser extension would scan the DOM for these markers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tree reconstruction&lt;/strong&gt;: The extension would rebuild the component hierarchy from the marker positions
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- What got injected --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;data-bdt-component=&lt;/span&gt;&lt;span class="s"&gt;"Counter"&lt;/span&gt; &lt;span class="na"&gt;data-bdt-file=&lt;/span&gt;&lt;span class="s"&gt;"Pages/Counter.razor"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked for simple cases. You could see your component tree in DevTools. But several problems emerged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Strict Parent Components
&lt;/h3&gt;

&lt;p&gt;Some component libraries enforce what types of children they accept. MudBlazor's &lt;code&gt;MudItem&lt;/code&gt; component, for example, validates its children and throws an exception if it encounters unexpected elements—like our invisible spans.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- This would throw an exception --&amp;gt;
&amp;lt;MudGrid&amp;gt;
    &amp;lt;MudItem&amp;gt; &amp;lt;!-- MudItem only accepts specific children --&amp;gt;
        &amp;lt;span data-bdt-component="..."&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;!-- 💥 Rejected --&amp;gt;
        &amp;lt;MyComponent /&amp;gt;
    &amp;lt;/MudItem&amp;gt;
&amp;lt;/MudGrid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a workaround: a &lt;code&gt;SkipComponents&lt;/code&gt; configuration that let users specify which components to exclude from marker injection. But this was a patch, not a solution. Users had to discover which components broke, then configure around them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: No Access to Runtime Data
&lt;/h3&gt;

&lt;p&gt;The span markers contained static metadata captured at build time. There was no connection to the actual running component instances. This meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No live parameter values&lt;/strong&gt;: You could see that a component &lt;em&gt;had&lt;/em&gt; a &lt;code&gt;Count&lt;/code&gt; parameter, but not its current value&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No performance metrics&lt;/strong&gt;: No way to track render counts or timing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No component state&lt;/strong&gt;: The markers were dead HTML, disconnected from the living Blazor application&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Problem 3: DOM Pollution
&lt;/h3&gt;

&lt;p&gt;Injecting markers into the DOM, even invisible ones, felt wrong. It modified the application's output, potentially affecting CSS selectors, automated tests, or edge cases I hadn't considered.&lt;/p&gt;

&lt;p&gt;I knew the architecture needed to change fundamentally. The question was: how?&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Core Problem
&lt;/h2&gt;

&lt;p&gt;At its heart, I needed to answer two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Which component rendered this DOM element?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What are the current parameter values for component X?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Blazor doesn't expose APIs to answer either question. The component tree exists in .NET memory, the DOM exists in the browser, and there's no public bridge between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Blazor Actually Works
&lt;/h3&gt;

&lt;p&gt;To find a solution, I needed to understand Blazor's rendering pipeline. Here's the simplified flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│ .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                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key insight: Blazor maintains an internal mapping between &lt;code&gt;componentId&lt;/code&gt; (an integer assigned to each component instance) and its DOM location. This mapping exists in the JavaScript runtime's &lt;code&gt;BrowserRenderer&lt;/code&gt;, but it's private.&lt;/p&gt;

&lt;p&gt;Every component gets a &lt;code&gt;componentId&lt;/code&gt; when created. This ID flows through the render batch to JavaScript. If I could intercept component creation and correlate it with what JavaScript sees, I'd have my bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Options I Explored
&lt;/h2&gt;

&lt;p&gt;I spent considerable time exploring approaches that ultimately didn't work. Here's what I learned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Intercept the MSBuild Pipeline
&lt;/h3&gt;

&lt;p&gt;My first thought was to leverage the existing CSS isolation mechanism. Blazor already adds empty &lt;code&gt;b-xxxxxxxxxx&lt;/code&gt; attributes to elements for scoped CSS. Could I piggyback on this?&lt;/p&gt;

&lt;p&gt;The problem: the Razor compilation is a black box. You provide &lt;code&gt;.razor&lt;/code&gt; files, MSBuild invokes the compiler, and you get compiled output. There's no hook to intercept the middle of this process and inject additional attributes. The CSS isolation attributes are added by the compiler itself, not by an extensible pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Force a Base Class
&lt;/h3&gt;

&lt;p&gt;What if I required all components to inherit from a &lt;code&gt;BdtComponentBase&lt;/code&gt; instead of &lt;code&gt;ComponentBase&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User would have to change every component to this&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BdtComponentBase&lt;/span&gt; &lt;span class="c1"&gt;// instead of ComponentBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User friction&lt;/strong&gt;: Requiring changes to every component is a non-starter for adoption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inheritance conflicts&lt;/strong&gt;: Many users already inherit from custom base classes in their projects, or from base classes provided by component libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party components&lt;/strong&gt;: You can't modify MudBlazor or Radzen components to inherit from your base class&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 3: Source Generator Lifecycle Injection
&lt;/h3&gt;

&lt;p&gt;A source generator could potentially inject registration code into lifecycle methods like &lt;code&gt;OnInitialized&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated code injected into component&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BdtRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problems multiply quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing overrides&lt;/strong&gt;: If the user already overrides &lt;code&gt;OnInitialized&lt;/code&gt;, you have a conflict. Do you wrap theirs? What about the call order?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial classes&lt;/strong&gt;: Razor components are already partial classes with generated code. Adding more generated lifecycle overrides creates complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases&lt;/strong&gt;: What about components that override &lt;code&gt;SetParametersAsync&lt;/code&gt; and never call base? What about components that don't inherit from &lt;code&gt;ComponentBase&lt;/code&gt; at all?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 4: Fork the Blazor SDK
&lt;/h3&gt;

&lt;p&gt;The nuclear option: create a modified Blazor SDK that includes tracking instrumentation.&lt;/p&gt;

&lt;p&gt;This is technically possible but practically terrible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance burden&lt;/strong&gt;: You'd need to track every Blazor release and merge changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User friction&lt;/strong&gt;: Users would need to reference a custom SDK instead of the official one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust&lt;/strong&gt;: Asking users to replace core framework components is a big ask&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 5: Inherit from the Renderer
&lt;/h3&gt;

&lt;p&gt;Blazor's &lt;code&gt;Renderer&lt;/code&gt; class is where the magic happens. If I could subclass it and add tracking...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BdtRenderer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebAssemblyRenderer&lt;/span&gt; &lt;span class="c1"&gt;// ❌ This doesn't work&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IComponent&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Track it here!&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, you can't specify your own renderer. The renderer is instantiated internally by the framework. There's no &lt;code&gt;services.AddSingleton&amp;lt;Renderer, MyRenderer&amp;gt;()&lt;/code&gt; pattern here. Looking at the source code, the constructors are public, but the actual instantiation is buried in infrastructure code that doesn't consult the DI container for the renderer itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 6: Metadata-Only Source Generator
&lt;/h3&gt;

&lt;p&gt;What if I just generated static metadata at compile time—a registry mapping component types to their source files and parameter definitions?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated at compile time&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BdtMetadata&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ComponentInfo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Components&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ComponentInfo&lt;/span&gt; 
        &lt;span class="p"&gt;{&lt;/span&gt; 
            &lt;span class="n"&gt;SourceFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Pages/Counter.razor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"IncrementAmount"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you static information, but still doesn't solve the runtime problem. You'd know that &lt;code&gt;Counter&lt;/code&gt; components exist and have an &lt;code&gt;IncrementAmount&lt;/code&gt; parameter, but you couldn't answer "what's the current value of &lt;code&gt;IncrementAmount&lt;/code&gt; for the &lt;code&gt;Counter&lt;/code&gt; instance at DOM position X?"&lt;/p&gt;

&lt;p&gt;The fundamental issue is &lt;strong&gt;componentId correlation&lt;/strong&gt;. JavaScript sees &lt;code&gt;componentId&lt;/code&gt; values in render batches. .NET has component instances. But connecting a specific &lt;code&gt;componentId&lt;/code&gt; to its &lt;code&gt;Type&lt;/code&gt; and live instance requires runtime tracking that this approach doesn't provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: IComponentActivator
&lt;/h2&gt;

&lt;p&gt;Deep in the Blazor source code, I found a seam: &lt;code&gt;IComponentActivator&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IComponentActivator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IComponent&lt;/span&gt; &lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;componentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This interface exists specifically to allow customization of component instantiation. When Blazor needs to create a component, it calls &lt;code&gt;IComponentActivator.CreateInstance()&lt;/code&gt;. By default, it uses a &lt;code&gt;DefaultComponentActivator&lt;/code&gt; that simply calls &lt;code&gt;Activator.CreateInstance()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the critical code from &lt;code&gt;Renderer.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IComponentActivator&lt;/span&gt; &lt;span class="nf"&gt;GetComponentActivatorOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IComponentActivator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;DefaultComponentActivator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If you register an &lt;code&gt;IComponentActivator&lt;/code&gt; in the DI container, Blazor will use it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the open window I needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The componentId Timing Problem
&lt;/h3&gt;

&lt;p&gt;There's the catch. When &lt;code&gt;CreateInstance&lt;/code&gt; is called, the component doesn't have a &lt;code&gt;componentId&lt;/code&gt; yet. The sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Renderer calls &lt;code&gt;IComponentActivator.CreateInstance(typeof(Counter))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Activator creates and returns the instance&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Then&lt;/em&gt; the Renderer assigns a &lt;code&gt;componentId&lt;/code&gt; and stores it in the component's &lt;code&gt;RenderHandle&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So in &lt;code&gt;CreateInstance&lt;/code&gt;, I have the &lt;code&gt;Type&lt;/code&gt; and the instance, but not the &lt;code&gt;componentId&lt;/code&gt;. How do I correlate them later?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lazy Resolution Trick
&lt;/h3&gt;

&lt;p&gt;Here's the insight: the &lt;code&gt;componentId&lt;/code&gt; is stored in a private field (&lt;code&gt;_renderHandle&lt;/code&gt;) on &lt;code&gt;ComponentBase&lt;/code&gt;. Once it's assigned (which happens before &lt;code&gt;OnInitialized&lt;/code&gt;), I can extract it via reflection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ExtractComponentId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IComponent&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ComponentBase&lt;/span&gt; &lt;span class="n"&gt;componentBase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;renderHandleField&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComponentBase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_renderHandle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonPublic&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;renderHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderHandleField&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;componentBase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderHandle&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;componentIdProperty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RenderHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ComponentId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonPublic&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;componentIdProperty&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderHandle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strategy becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On creation&lt;/strong&gt;: Track the instance in a "pending" collection (we have Type and instance, but no componentId)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On query&lt;/strong&gt;: When JavaScript asks about a componentId, scan pending instances, extract their componentIds via reflection, and find the match&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This lazy resolution means we don't pay the reflection cost until someone actually queries for component information.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Pillar Architecture
&lt;/h2&gt;

&lt;p&gt;The v0.10 architecture has three distinct layers that work together:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pillar 1: Compile-Time Metadata (Source Generator)
&lt;/h3&gt;

&lt;p&gt;A source generator scans all &lt;code&gt;.razor&lt;/code&gt; files during build and generates a static registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated code&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BdtComponentMetadata&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ComponentInfo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Registry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ComponentInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;SourceFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Pages/Counter.razor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;LineNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"IncrementAmount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"int"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// ... all other components&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides static metadata with zero runtime overhead for the scanning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pillar 2: Runtime Tracking (IComponentActivator + Registry)
&lt;/h3&gt;

&lt;p&gt;The custom activator intercepts every component creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BdtComponentActivator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComponentActivator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;BdtRegistry&lt;/span&gt; &lt;span class="n"&gt;_registry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IComponent&lt;/span&gt; &lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;componentType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;componentType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;IComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrackPendingInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;componentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry maintains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pending instances&lt;/strong&gt;: Components we've seen but haven't resolved componentIds for&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolved instances&lt;/strong&gt;: componentId → Type → WeakReference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When queried, it can provide live parameter values by reflecting over the actual component instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pillar 3: Browser-Side Interception (JavaScript)
&lt;/h3&gt;

&lt;p&gt;A JavaScript module (auto-loaded via Blazor's JavaScript initializer mechanism) intercepts render batches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BlazorDeveloperTools.lib.module.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;afterStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blazor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRenderBatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Blazor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderBatch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;Blazor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderBatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// See which componentIds were updated&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updatedIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRenderBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Query .NET for component info&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;updatedIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;DotNet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invokeMethodAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BlazorDeveloperTools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GetComponentInfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="nx"&gt;id&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Send to DevTools panel&lt;/span&gt;
            &lt;span class="nf"&gt;notifyDevTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Call original&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalRenderBatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JavaScript initializer mechanism (&lt;code&gt;*.lib.module.js&lt;/code&gt; files in wwwroot) means this loads automatically—no script tags required from the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Architecture Works
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;How It's Met&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No user code changes&lt;/td&gt;
&lt;td&gt;✓ Just add NuGet package and one service registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with any component library&lt;/td&gt;
&lt;td&gt;✓ No DOM injection, intercepts at framework level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live parameter values&lt;/td&gt;
&lt;td&gt;✓ Reflection on actual component instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component tree accuracy&lt;/td&gt;
&lt;td&gt;✓ Every component flows through the activator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance metrics (future)&lt;/td&gt;
&lt;td&gt;✓ Can track render timing via batch interception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory safe&lt;/td&gt;
&lt;td&gt;✓ WeakReferences prevent memory leaks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What About Conflicts?
&lt;/h3&gt;

&lt;p&gt;One concern: what if another library also registers an &lt;code&gt;IComponentActivator&lt;/code&gt;? &lt;/p&gt;

&lt;p&gt;In practice, this is extremely rare. The only library I'm aware of that uses this is bUnit (for testing). And if a conflict does occur, activators can be chained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddBlazorDevTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existingActivator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IComponentActivator&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Chain to existing if present&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IComponentActivator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BdtComponentActivator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BdtRegistry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;existingActivator&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;ImplementationInstance&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;IComponentActivator&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The User Experience
&lt;/h2&gt;

&lt;p&gt;For developers using Blazor Developer Tools v0.10, the experience is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"BlazorDeveloperTools"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"0.10.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorDevTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No base classes. No MSBuild configuration. No skipping problematic components. It just works—with MudBlazor, Radzen, Syncfusion, or any other component library.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The v0.10 architecture unlocks capabilities that were impossible with the span marker approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live parameter values&lt;/strong&gt;: See the actual current state of component parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render tracking&lt;/strong&gt;: Count how many times each component renders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance profiling&lt;/strong&gt;: Measure render duration and identify bottlenecks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State snapshots&lt;/strong&gt;: Capture component state at points in time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why did this render?"&lt;/strong&gt;: Trace what triggered a re-render&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The marker approach could only ever show you the tree. This architecture gives us access to the living application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Sometimes the right solution requires going deeper. The v0.9 span markers were a clever hack, but they fought against Blazor rather than working with it. The v0.10 architecture integrates at the framework level through an official extension point.&lt;/p&gt;

&lt;p&gt;The lesson: when you hit a wall, study the source code. Somewhere in there, there might be a seam that the framework authors left open—intentionally or not.&lt;/p&gt;

&lt;p&gt;Blazor Developer Tools v0.10 is currently in development. Follow the progress on &lt;a href="https://github.com/joe-gregory/blazor-devtools" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or try the current v0.9 release while the new architecture takes shape.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Joseph Gregory is a .NET developer and creator of Blazor Developer Tools. Follow him on &lt;a href="https://x.com/joegregorydev" rel="noopener noreferrer"&gt;X&lt;/a&gt; for updates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>dotnet</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Building Blazor Developer Tools: 7 Failed Attempts and What I Learned</title>
      <dc:creator>Joseph Gregory</dc:creator>
      <pubDate>Thu, 13 Nov 2025 03:47:47 +0000</pubDate>
      <link>https://forem.com/joseph_gregory_c4be294afc/building-blazor-developer-tools-7-failed-attempts-and-what-i-learned-3lm</link>
      <guid>https://forem.com/joseph_gregory_c4be294afc/building-blazor-developer-tools-7-failed-attempts-and-what-i-learned-3lm</guid>
      <description>&lt;h1&gt;
  
  
  Building Blazor Developer Tools: 7 Failed Attempts and What I Learned
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every major frontend framework has developer tools - React has React DevTools, Vue has Vue Devtools. Blazor didn't. As someone who loves working in Blazor, I wanted to change that.&lt;/p&gt;

&lt;p&gt;The goal was simple: let developers inspect Razor component markup in the browser, just like you can with React components. The implementation? Not so simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Challenge
&lt;/h2&gt;

&lt;p&gt;To map rendered HTML back to Razor source code, I needed to inject markers into the markup. Seems straightforward, right? Add some HTML comments or data attributes during compilation, read them in the browser, done.&lt;/p&gt;

&lt;p&gt;Except Blazor's Razor compilation pipeline had other ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #1-6: Dead Ends
&lt;/h2&gt;

&lt;p&gt;I tried various approaches over several weeks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML comments&lt;/strong&gt;: Don't survive the Razor compilation process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data attributes on existing elements&lt;/strong&gt;: Too invasive, changes developer's HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Razor directives&lt;/strong&gt;: Couldn't access the right compilation stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-compilation IL weaving&lt;/strong&gt;: Too fragile, broke with framework updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime component wrapping&lt;/strong&gt;: Performance nightmare&lt;/li&gt;
&lt;li&gt;&lt;em&gt;And two other approaches I'm embarrassed to even mention&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each attempt taught me more about Blazor's internals, but none gave me the clean solution I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough: Shadow Copies + Span Markers
&lt;/h2&gt;

&lt;p&gt;The working solution came from combining two insights:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Don't modify the developer's files
&lt;/h3&gt;

&lt;p&gt;Create shadow copies of Razor files during build, inject markers into those copies, and let them go through the normal Razor pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use invisible span elements as markers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Developer writes: --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;MyComponent&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Shadow copy after marker injection: --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt; &lt;span class="na"&gt;data-bdt=&lt;/span&gt;&lt;span class="s"&gt;"Component:MyComponent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;MyComponent&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MSBuild task creates these shadow copies, the Razor SDK processes them normally, and the browser extension reads the markers to reconstruct the component tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limitation I Didn't Foresee
&lt;/h2&gt;

&lt;p&gt;This works beautifully... until it doesn't.&lt;/p&gt;

&lt;p&gt;Some Blazor components validate their children. For example, &lt;code&gt;MatTable&lt;/code&gt; from MatBlazor expects only &lt;code&gt;MatTableRow&lt;/code&gt; children. When it sees my span marker, it throws an exception.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This component would break:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MatTable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="n"&gt;My&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="n"&gt;causes&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="p"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MatTableRow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;...&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;MatTableRow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;MatTable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Current workaround:&lt;/strong&gt; Let developers exclude specific components in their &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;BlazorDevToolsExclude&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"MatTable;AnotherComponent"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, but it's not the &lt;em&gt;extremely painless experience&lt;/em&gt; I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vision for V2: A Custom SDK
&lt;/h2&gt;

&lt;p&gt;The real solution became clear after shipping v0.9: &lt;strong&gt;I need to build a custom Razor SDK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of injecting visible elements, the SDK would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mirror the official &lt;code&gt;Microsoft.NET.Sdk.Razor&lt;/code&gt; in all behaviors and versioning&lt;/li&gt;
&lt;li&gt;Add invisible metadata during compilation using Blazor's existing &lt;code&gt;b-*&lt;/code&gt; attribute pattern&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;bdt-*&lt;/code&gt; attributes that follow the same logic as Blazor's internal markers
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- V2 approach - invisible to components: --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;bdt-component=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent"&lt;/span&gt; &lt;span class="na"&gt;bdt-line=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;MyComponent&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is better:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No DOM pollution&lt;/strong&gt; - attributes don't affect component children validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance data&lt;/strong&gt; - SDK can inject timing/render info&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live parameter values&lt;/strong&gt; - Can expose component state like JavaScript debuggers do&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version alignment&lt;/strong&gt; - Stays in sync with Blazor releases automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Shipping Imperfect Software
&lt;/h2&gt;

&lt;p&gt;I shipped v0.9 knowing these limitations exist. Here's why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Discipline demands shipping sooner rather than perfectly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The current version works for 90% of Blazor apps. The workaround handles edge cases. Most importantly, shipping let me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate that developers actually want this tool&lt;/li&gt;
&lt;li&gt;Get real-world feedback on what matters most&lt;/li&gt;
&lt;li&gt;Build credibility in the community before asking for contributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK approach is the right long-term solution, but it's months of work. I needed to know people cared before investing that time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Blazor Developer Tools 0.9.x is live and free on GitHub. It has rough edges, but it's useful today.&lt;/p&gt;

&lt;p&gt;Version 1.0 with the custom SDK is the next mountain to climb. If you're interested in following along or contributing, check out the repo at &lt;a href="https://github.com/joe-gregory/blazor-devtools" rel="noopener noreferrer"&gt;github.com/joe-gregory/blazor-devtools&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Tool Builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shadow copies solved the "don't modify source" problem&lt;/strong&gt; - useful pattern for any build-time code injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blazor's compilation pipeline is less documented than its runtime&lt;/strong&gt; - expect to read source code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the imperfect version&lt;/strong&gt; - I learned more in two weeks after launch than in two months of private development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some problems need infrastructure solutions&lt;/strong&gt; - Sometimes the "hack" reveals you need to go deeper into the stack&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Building developer tools is humbling. You're building for people who will immediately see your shortcuts and limitations. But it's also deeply rewarding - every GitHub star represents someone whose workflow you improved.&lt;/p&gt;

&lt;p&gt;If you work with Blazor, I'd love your feedback. And if you've built devtools for other frameworks, I'd love to hear your war stories in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Joe Gregory is a senior .NET developer and the creator of Blazor Developer Tools. He's currently planning the architecture for v1.0 while his daughter watches Bluey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
