Skip to content

C#: Blazor @inject in .razor never reaches the cross-file type resolver — one stub node per file, emitted as imports instead of references #3187

Description

@Bridgeway

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions