[release/stable] Resolve consecutive WithoutEndTag tag helpers as siblings - #84782
Merged
Conversation
|
Azure Pipelines: Successfully started running 2 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
## Summary Consecutive tag helpers declared with `TagStructure.WithoutEndTag` and written without a self-closing slash (e.g. `<meta-description><meta-keywords><head-custom>`) only bind the **first** helper. The rest are emitted as literal, unprocessed markup. This is a regression from the deferred tag helper lowering work (dotnet/razor#12957), which moved tag helper resolution after IR lowering. It was reported downstream as [dotnet/aspnetcore#68193](https://github.com/dotnet/aspnetcore/issues/68193): a GrandNode app rendered a blank page (`Uncaught ReferenceError: Vue is not defined`) because the `<head>` script-registration tag helpers silently stopped running and the Vue bundle was never emitted. ## Root cause The HTML parser nests consecutive unclosed tags, so `<a><b><c>` parses as `a > b > c`. When `DefaultTagHelperResolutionPhase.ResolveElement` binds `a` as a `StartTagOnly` helper, it promotes `a`'s parser-nested body children to be **siblings** inserted *after* `a`. But the element walker `ResolveElements` iterates children in reverse, so it has already moved past that position and never resolves the promoted siblings. They remain `UnresolvedElementIntermediateNode`s and are later unwrapped to literal HTML. The `ConvertToPlainElement` path already re-resolves its promoted siblings; the direct `ResolveElement` `StartTagOnly` path was missing the equivalent step. ## Fix After promoting the `StartTagOnly` element's children to siblings, resolve them in place (in reverse, since resolving a promoted `StartTagOnly` sibling can itself insert further siblings). This mirrors the existing pattern in `ConvertToPlainElementAndResolve`. ## Testing Two new integration tests in `TagHelpersIntegrationTest`: - `ConsecutiveWithoutEndTagTagHelpers_AllBind` -- three consecutive `WithoutEndTag` helpers all bind. - `MixedNestedStartTagOnlyAndHtmlTagHelpers_AllResolveCorrectly` -- a tangled mix of a nestable helper, consecutive `WithoutEndTag` helpers, a normal helper, and real HTML (`<section>`, `<div>`); asserts every helper binds in document order and real markup is preserved. Both fail without the fix (only the first helper binds) and pass with it (net10.0 and net472). > Note: this regression is also present in the shipping .NET 10 GA Razor compiler, so a servicing backport is likely warranted. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/roslyn/pull/84771) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e4a482bf-ffc0-4a98-a673-863b5e84c6d8
The backport squash kept release/stable's using block, dropping the Microsoft.AspNetCore.Razor.Language.Intermediate and Roslyn.Test.Utilities imports the tests need, which broke the build with CS0246 on TagHelper IR types and the WorkItem attribute. Add them back. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e4a482bf-ffc0-4a98-a673-863b5e84c6d8
chsienki
force-pushed
the
backport/pr-84771-to-release/stable
branch
from
August 7, 2026 22:00
1af7306 to
5eb281a
Compare
davidwengier
approved these changes
Aug 7, 2026
This was referenced Aug 12, 2026
Closed
4 tasks
gunndabad
added a commit
to DFE-Digital/teaching-record-system
that referenced
this pull request
Aug 13, 2026
### Context
Every `Tests (SupportUi.EndToEndTests)` run on CI has failed since
2026-08-11 20:40. It looks like flakiness — 13 of 142 tests fail, the
rest pass — but the split is deterministic: the failures are exactly the
tests that touch an accessible-autocomplete field (all 6 in
`AddRouteToProfessionalStatusTests`, the 5 autocomplete ones in
`EditRouteToProfessionalStatusTests`, plus `AddMq` and
`EditMqProvider`).
`global.json` pinned `"version": "10"`, which floats to any 10.x SDK, so
CI started using **SDK 10.0.400** the day it was released (2026-08-11) —
exactly when the failures began.
**Root cause (upstream):** on 10.0.400, only the *first* tag-helper
element inside a `@section` block is bound. Every subsequent element in
that section is emitted as literal markup, so `~/` is never resolved.
The build succeeds with no warning.
In `SupportUi/Pages/Shared/_Layout.cshtml`, `@section Head` opens with
`<meta name="htmx-config" …>` — that one binds, and everything after it
does not:
| SDK | Generated | Rendered |
| --- | --- | --- |
| 10.0.301 / 10.0.303 | `CreateTagHelper<UrlResolutionTagHelper>()` |
`<link href="/app.657ritalq7.css">` |
| 10.0.400 | `WriteLiteral("~/app.css")` | `<link href="~/app.css">` |
The browser resolves the literal `~/…` relative to the current page, so
`/routes/add/~/Components/accessible-autocomplete.min.js` 404s,
`accessibleAutocomplete` is undefined, the page's `window.onload`
handler throws, and the `<select>` is never enhanced into `input#{id}` —
which is what the tests wait for.
**Upstream tracking:**
[dotnet/razor#13216](dotnet/razor#13216),
[#13217](dotnet/razor#13217),
[#13218](dotnet/razor#13218) — all closed as
fixed by
[dotnet/roslyn#84771](dotnet/roslyn#84771),
backported to the 10.0.4xx band in
[dotnet/roslyn#84782](dotnet/roslyn#84782). The
fix has **not shipped yet** — 10.0.400 is still the only 10.0.4xx SDK
released.
### Changes proposed in this pull request
Pin `global.json` to the 10.0.3xx feature band:
```json
{ "sdk": { "version": "10.0.300", "rollForward": "latestPatch" } }
```
10.0.303 shipped the same day as 10.0.400 on the **same 10.0.11
runtime**, so this costs nothing but the SDK feature band — we stay on
the current runtime and keep getting patch updates within 10.0.3xx.
Since the upstream fix is already merged and backported, this pin should
be short-lived: drop it once a 10.0.4xx containing roslyn#84782 is
released.
### Guidance to review
**Exact blast radius.** I built both web projects under 10.0.400 with
`EmitCompilerGeneratedFiles` and grepped the generated code. Affected: 5
URLs, all in `SupportUi/Pages/Shared/_Layout.cshtml` (`app.css`, both
`accessible-autocomplete.min.*`, `moj-frontend-9.0.0.min.css`,
`htmx.min.js`). **AuthorizeAccess is not affected** — in both of its
`@section` blocks the `~/` element happens to be the first tag helper,
so it still binds. No `asp-*` attributes leak anywhere in either
project.
**This is a production bug, not just a test bug.** SupportUi built with
10.0.400 serves a page with no CSS and no working JS. We're only safe
today because the `Dockerfile` pins the SDK image by digest (unchanged
since June, so still 10.0.3xx). When dependabot next bumps that digest
past 10.0.400, the build would have silently shipped broken assets —
with this pin it fails loudly at build time instead.
**Narrower fixes were tried and rejected:**
- Adding `asp-append-version="true"` to force a tag-helper binding — no
effect, because the element still isn't the *first* tag helper in the
section. This is what ruled out a per-element workaround.
- Rewriting `~/` to `/` — works, but loses static-asset fingerprinting,
and would need re-checking against every future SDK.
### Verification
- Reproduced locally by installing SDK 10.0.400 — the same 6
`AddRouteToProfessionalStatusTests` failed with the identical Playwright
errors seen on CI.
- Confirmed SDK 10.0.303 renders the URLs correctly, and that the pin
rejects 10.0.400.
- `just build` — 0 errors.
- All 142 `SupportUi.EndToEndTests` pass.
- `just format-changed` — no changes.
### Checklist
- [ ] Attach to Trello card
- [x] Rebased master
- [x] Cleaned commit history
- [x] Tested by running locally
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #84771 to release/stable
/cc @chsienki
Customer Impact
Customers with multiple, sequential, non-closed tag helpers only have the first one rendered. Meaning pages render incorrectly.
This is a very common pattern as several of the out of the box taghelpers ship in this format and are often combined together.
See:
@sectionblocks only bind on the first matching element (regression in SDK 10.0.400) razor#13218Regression
dotnet/razor#12957
Testing
Added unit tests. CTI caught the regression in .NET11 P7, but it existed as early as P4. Unsure why it wasn't caught earlier.
Risk
Low: an extra code path that only runs for this specific scenario, not able to regress other previously working paths. Added tests to cover, and all previous tests pass.
Microsoft Reviewers: Open in CodeFlow