A Razor comment is a comment between elements. Between a tag's attributes it is an attribute name, and nothing in the toolchain says so.
@* fine — above the tag *@
<FluentButton Class="@Class"
@* NOT fine — this is an attribute name *@
IconEnd="@icon" />
Why nothing catches it
| Instrument | What it does with the shape |
|---|---|
| the Release build, warnings as errors | compiles it, silently — an unmatched attribute is legal Razor |
a static render (HtmlRenderer) |
writes the comment text into the markup as a bare attribute |
| a rendered-HTML assertion over that markup | passes, unless it happens to look for this |
| the interactive renderer | applies it via setAttribute → InvalidCharacterError |
The last row is the whole defect. The throw happens inside
Renderer.InvokeRenderCompletedCallsAfterUpdateDisplayTask, which Blazor treats as an unrecoverable
circuit fault: it logs Unhandled exception in circuit … and closes the circuit. The browser is
left showing whatever had already painted, plus "An unhandled error has occurred. Try reloading the
page." — and every later click is a no-op, because nothing on the page is live any more.
So the symptom does not look like a markup bug. It looks like a button that does nothing.
Measured, twice in 48 hours
MenuItemView.razor (2026-09-11) put one between Class and IconEnd on the split button's
chevron half. Every Store cover in MeshWeaver.Education's disposable-mesh gate painted its hero,
re-rendered into that split button as the learner's own data arrived, and died. All four install
shards plus the blocking install failed; the e2e reported "no install dialog within 120 s of the
click — still on the cover, the click did nothing", and the failure screenshot is an ordinary
course cover with the yellow error bar along the bottom (Education run 34671104516). Education's
main stayed red on it.
DateTimeView.razor (2026-09-10) put one before aria-label on FluentDatePicker — found by
the sweep that followed the first, same shape, same consequence for every form carrying a date
field.
Two occurrences, two days, two unrelated files, both merged green.
What now refuses it
scripts/check-razor-in-tag-comments.py, run in CI with--self-testfirst. It scans every.razorundersrc/, tracking whether it is inside a tag and outside a quoted value — a value may legally hold@,*,<and>, and a@(…)expression may nest a quote of the same kind that opened the value, so both are skipped as units. It prints the denominator with the verdict, and a run that reads zero files is a RED, not a pass.SplitButtonChevronTest.EveryEmittedAttributeNameIsOneABrowserAccepts, which asserts over the emitted markup of the one component that shipped the defect. Useful, but it covers one component; the script is what covers the class.
This repo is the fleet's whole exposure: core carries zero .razor files under src/ — all
Blazor markup lives here (183 files at the time of writing), so the script's denominator is the
denominator.
When you want a comment there anyway
Put it above the tag and name the attribute it is about. If it must sit closer, a C# //
comment inside a @(…) expression is fine — that is C#, not markup.
🚨 One trap while writing the comment you just moved: Razor closes a comment at the first
closing token, so a comment that quotes that token ends early and the remainder leaks into the
markup — which is its own build break (RZ1003). Describe the shape in words instead of quoting the
closing token.