Summary
Blazor @inject directives in .razor files never reach the C# cross-file type resolver (#1466). Each injected type gets a fresh file-local stub node per .razor file, emitted as an imports edge rather than a references edge to the resolved definition. The same type injected via a constructor in a .cs file resolves correctly, so this is a file-type coverage gap, not a type-kind one.
This is distinct from the BCL/stdlib case reported in #1318 (comment by @kbrilla): the types affected here are project-defined classes with a canonical definition node already in the graph. The resolver simply never runs on .razor.
Version: 0.9.51 (also reproduced on 0.9.50). Windows 11, Python 3.10.
Reproduction
Five files, no project scaffolding needed:
Services/WidgetService.cs
namespace Demo.Services;
public class WidgetService
{
public string GetName() => "widget";
public int Count() => 1;
}
Services/Consumer.cs — the control, constructor injection in .cs
namespace Demo.Services;
public class Consumer
{
private readonly WidgetService _widgets;
public Consumer(WidgetService widgets) => _widgets = widgets;
public string Use() => _widgets.GetName();
}
Pages/AlphaPage.razor (and BetaPage.razor, GammaPage.razor, identical but for the route)
@page "/alpha"
@inject WidgetService _widgets
<p>@_widgets.GetName()</p>
from graphify.extract import collect_files, extract
from pathlib import Path
r = extract(collect_files(Path('.')), cache_root=Path('.'), parallel=False)
Actual
Four nodes for one class:
id=pages_alphapage_razor_widgetservice deg=1 src=Pages/AlphaPage.razor
id=pages_betapage_razor_widgetservice deg=1 src=Pages/BetaPage.razor
id=pages_gammapage_razor_widgetservice deg=1 src=Pages/GammaPage.razor
id=services_widgetservice_demo_services_widgetservice deg=4 src=Services/WidgetService.cs
Edges on the canonical node — the .cs control resolves correctly:
Consumer --references--> WidgetService [EXTRACTED] src=Services/Consumer.cs
WidgetService.cs --contains--> WidgetService [EXTRACTED]
WidgetService --method--> .GetName() [EXTRACTED]
WidgetService --method--> .Count() [EXTRACTED]
Edges on the razor stubs — file-local, never re-pointed:
pages_alphapage --imports--> pages_alphapage_razor_widgetservice
pages_betapage --imports--> pages_betapage_razor_widgetservice
pages_gammapage --imports--> pages_gammapage_razor_widgetservice
Expected
Each @inject should emit a references edge from the component to services_widgetservice_demo_services_widgetservice, as constructor injection does from Consumer.cs. No file-local stub.
Impact at scale
On a real Blazor WASM solution (943 code files: 721 .cs, 196 .razor; 14,328-node graph):
- One DI service class → 91 nodes: 1 canonical at degree 15, plus 90 orphans at degree 1, all 90 in
.razor, 0 in .cs.
- A widely-injected interface → 96 nodes, none above degree 1 — no canonical node survives at all.
- Across project-defined service types: 99 razor orphans, 0
.cs orphans. Clean split along file type.
- 95% of all
.razor nodes end up degree ≤1 (3,716 of 3,919), against 64% for .cs.
- Graph-wide, 380 labels appear >5× as separate nodes, involving 8,420 nodes = 59% of the graph.
Downstream this inflates the community count, produces thousands of spurious "isolated nodes", and makes centrality/cohesion figures unusable — a DbContext community scored 0.0135 cohesion purely because it was a star of unlinked stubs. Analytically, the effect is that a shared service looks unused rather than central.
Notes
Related but not the same: #1318 (umbrella; the BCL/stdlib branch of this), #207 (proposed canonicalization phase), #1186. If you would rather fold this into #1318 as another branch of the umbrella, happy for it to be closed as a duplicate — filing separately since the .razor/.cs split is a concrete, testable boundary that the existing reports don't name.
Summary
Blazor
@injectdirectives in.razorfiles never reach the C# cross-file type resolver (#1466). Each injected type gets a fresh file-local stub node per.razorfile, emitted as animportsedge rather than areferencesedge to the resolved definition. The same type injected via a constructor in a.csfile resolves correctly, so this is a file-type coverage gap, not a type-kind one.This is distinct from the BCL/stdlib case reported in #1318 (comment by @kbrilla): the types affected here are project-defined classes with a canonical definition node already in the graph. The resolver simply never runs on
.razor.Version: 0.9.51 (also reproduced on 0.9.50). Windows 11, Python 3.10.
Reproduction
Five files, no project scaffolding needed:
Services/WidgetService.csServices/Consumer.cs— the control, constructor injection in.csPages/AlphaPage.razor(andBetaPage.razor,GammaPage.razor, identical but for the route)Actual
Four nodes for one class:
Edges on the canonical node — the
.cscontrol resolves correctly:Edges on the razor stubs — file-local, never re-pointed:
Expected
Each
@injectshould emit areferencesedge from the component toservices_widgetservice_demo_services_widgetservice, as constructor injection does fromConsumer.cs. No file-local stub.Impact at scale
On a real Blazor WASM solution (943 code files: 721
.cs, 196.razor; 14,328-node graph):.razor, 0 in.cs..csorphans. Clean split along file type..razornodes end up degree ≤1 (3,716 of 3,919), against 64% for.cs.Downstream this inflates the community count, produces thousands of spurious "isolated nodes", and makes centrality/cohesion figures unusable — a
DbContextcommunity scored 0.0135 cohesion purely because it was a star of unlinked stubs. Analytically, the effect is that a shared service looks unused rather than central.Notes
Related but not the same: #1318 (umbrella; the BCL/stdlib branch of this), #207 (proposed canonicalization phase), #1186. If you would rather fold this into #1318 as another branch of the umbrella, happy for it to be closed as a duplicate — filing separately since the
.razor/.cssplit is a concrete, testable boundary that the existing reports don't name.