Repository navigation
Restore public GetCSharpDocument() and GeneratedCode for runtime compilation - #84813
Conversation
…ilation The decl/impl split moved the code document's C# accessor to an internal declaration-aware overload and renamed the generated-code string to a SourceText, removing two public members that ASP.NET Core's runtime Razor view compiler consumes. Both are still part of the shipped public surface, so external compilation breaks when the changes flow into the product. Re-expose them as thin shims over the new API: the parameterless GetCSharpDocument() returns the implementation document (the primary generated output, and the complete class for the non-component views runtime compilation handles), and GeneratedCode returns the document text as a string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b8188d9c-18d2-4e12-8b25-4c8d99114193
|
Azure Pipelines: Successfully started running 2 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR restores two previously-public Razor compiler APIs needed by ASP.NET Core’s runtime Razor view compilation path by reintroducing thin shims over the newer decl/impl-split API surface.
Changes:
- Reintroduce
RazorCodeDocumentExtensions.GetCSharpDocument()as a shim returning the implementation C# document. - Reintroduce
RazorCSharpDocument.GeneratedCodeas a shim overText.ToString()to preserve the legacy string-based accessor.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCSharpDocument.cs | Adds a GeneratedCode string shim over the existing Text API. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocumentExtensions.cs | Restores GetCSharpDocument() as a public extension returning the implementation document. |
| public static RazorCSharpDocument GetCSharpDocument(this RazorCodeDocument codeDocument) | ||
| => codeDocument.GetRequiredCSharpDocument(declarationDocument: false); |
| /// containing the render method and, for a non-component document, the complete class. A component | ||
| /// that split has its declaration surface in a separate declaration document; the implementation | ||
| /// document is the code the source generator emits as the document's primary output. |
|
Long-term follow-up to remove these compat shims (after migrating ASP.NET Core runtime compilation off them): #84814 |
davidwengier
left a comment
There was a problem hiding this comment.
So this makes the SDK compile, but does it not break things when the source generator is turned off, and the SDK only tries to compile in the impl half of a Razor document?
|
Nevermind, read the PR description. I didn't realise the SDK didn't handle .razor documents. |
|
Merging to unblock VMR Code flow. |
It does handle .razor documents, as you can compile a razor project without the source generator. That uses the APIs directly with its own project engine, so Sonic has no effect. These breaks were specifically in runtime compilation, for which only .cshtml is supported, so its safe to compile with the source generator and sonic on, and then runtime compilation over the top to replace the files at runtime. |
Backs the `features/sonic` decl/impl split out of `main` to unblock insertion. Reverts the merge (#84778) and the commits that stacked on top of it and cannot stand without it. ## What reverts - **#84778** `Merge/sonic into main` -- the whole feature body (reverted with `-m 1`, keeping pre-sonic `main`). - **#84818** fallback child-content discovery fix -- patches sonic's fast/slow discovery, which no longer exists. - **#84813** restore `GetCSharpDocument()`/`GeneratedCode` -- these were re-added to shim sonic's API removal; reverting the merge restores the originals, so the shim would double-define them. - **#84795** always pass C# formatting options -- edits `CSharpFormattingOptionsHelper.cs`, a file that only exists because of sonic. ## What is kept The other post-merge commits are independent of sonic and stay: arcade bumps, SDL param removal, CS8802, #84758 (syntax-tree sharing), and the two Razor features that merely shared files -- **#84760** (unbound-attribute warnings) and **#84764** (empty-outer-tag warning). Preserving #84760 needed one manual resolution in `UnresolvedAttributeIntermediateNode.cs`: sonic added the `CloneNode()`/`Clone()` overrides and #84760 later added one property assignment inside `CloneNode`. The overrides are sonic infrastructure and go with the revert; #84760's `IsDirectiveAttributeCandidate` property and all its real usages are untouched. ## Validation - `Razor.slnf` builds clean (compiler + all tooling projects, including the ones #84795 touched). - `Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests`: 215 passed. - `Microsoft.AspNetCore.Razor.Language.UnitTests` (component / tag helper / bind / unbound-attribute): 1593 passed -- confirms #84760's kept feature still works. ## Note Reverting the merge means re-landing `features/sonic` later will need this revert itself reverted first -- expected for a back-out. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/roslyn/pull/84831)
…#84844) Re-lands the sonic decl/impl split (backed out in #84831 to unblock insertions) together with the slow-discovery perf work (#84808) and a new fix that removes the remaining cold-allocation regression on fallback components. ## Why this is safe to re-land now The split was reverted because a VS insertion showed a cold-allocation regression on fallback-heavy scenarios (Component Classification, cohosting completion) with no offsetting speed gain. This branch addresses the root cause rather than just re-applying the revert. ## What's here 1. **Un-revert** of #84831 -- restores the sonic merge and the fixes that landed on top of it (#84818 fallback child-content discovery, #84813 RZC public-API compat, #84795 formatting options). 2. **#84808** -- slow discovery resolves fallback types via the declaration table (`GetSymbolsWithName`) instead of a per-tree semantic model, and discovers only the fallback types rather than re-walking the whole augmented compilation. (Removed the +535 MB semantic-model regression.) 3. **Reuse the initial parse for fallback discovery** (new) -- the remaining regression. ## The new fix A fallback component (an `@inherits`/`@implements`/`@typeparam` header, or body markup the analysis can't route) can't be split, so its descriptor comes from discovery over an augmented compilation. Each fallback component was being processed **twice**: the generation engine's initial parse, then a freshly created declaration engine that **re-parsed the same source** just to produce a discoverable decl. For an app where nearly every component `@inherits` a base (MudBlazor, OrchardCore) every component is a fallback, so that second parse dominates cold discovery allocation. The split phase already builds the discoverable declaration surface for a *split* component from the nodes it just classified. This builds the same surface for a *fallback* component and stashes it on the document node; the generator lowers it in place -- reusing the initial parse -- instead of re-parsing through a separate engine. It's never emitted to pre-compilation (the bodiless type shell still fills that role for C# type resolution); only its syntax tree feeds slow discovery. The rare shapes with no render method or namespace keep the re-parse path. ## Measurements Cold source-generator allocation over 100 fallback components (in-memory A/B): | | Allocated | |---|---| | Pre-split baseline | 17.1 MB | | Sonic + #84808 (before this fix) | 19.6 MB | | **This branch** | **13.6 MB** | The fallback path is now **below** the pre-split baseline -- a genuine win, not break-even. ## Validation - `Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests`: 219 passed / 1 skipped (incl. #84818's fallback child-content test). - `Microsoft.AspNetCore.Razor.Language.UnitTests` (component / tag helper / split / bind / discovery / clone): 1672 passed. - Razor compiler builds clean. Draft pending the RPS + Speedometer insertion run. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/roslyn/pull/84844) --- ## Update: warm/interactive path fix (html-span completion) A matched-baseline Speedometer re-run showed the cold-discovery regressions gone but a smaller residual on **html-span completion in a component** (`CLR_BytesAllocated_devenv`) -- the interactive/warm path, a different signature than the cold source-generator work above. Root cause: the fallback decl incremental pipeline compared decls by **instance**, while the split-decl (`DeclSources`) fast path compares by **text**. A fallback component's decl is markup-free and checksum-suppressed, so a markup-only edit (every keystroke during completion) yields a byte-identical decl -- but the instance changed each run, invalidating the parse and, through the `Collect`, re-running slow discovery over *every* fallback component on each edit. Fix: compare fallback decls by text (`Text.ContentEquals`), mirroring the fast path. A markup-only edit now leaves the fallback decl `Unchanged`, keeping its parse and slow discovery cached. Measured (100 fallback components, warm re-run on a markup edit): | | warm alloc | |---|---| | Pre-split baseline | 0.41 MB | | Before this fix | 3.2 MB (~8x) | | **After this fix** | **0.42 MB** | Guarded by `IncrementalCompilation_WhenFallbackComponentMarkupChanges_SlowDiscoveryStaysCached` (verified to fail without the comparer). Full SG suite 220 passed / 1 skipped.
Problem
The Razor decl/impl split (flowed into the VMR via dotnet/dotnet#8179) removed two public members that ASP.NET Core's runtime Razor view compiler (
Mvc.Razor.RuntimeCompilation.RuntimeViewCompiler) depends on, breaking its build in the product:RazorCodeDocumentExtensions.GetCSharpDocument(this RazorCodeDocument)→ replaced by the internal, declaration-awareRazorCodeDocument.GetCSharpDocument(bool declarationDocument).RazorCSharpDocument.GeneratedCode(string) → replaced bypublic SourceText Text.RuntimeViewCompiler.CompileAndEmitcallscodeDocument.GetCSharpDocument().GeneratedCode. Both members are still listed in the compiler's public API baselines, so this is an unshipped-public-API break rather than an internal refactor.Fix
Re-expose both as thin shims over the new API:
GetCSharpDocument()returns the implementation document (GetRequiredCSharpDocument(declarationDocument: false)).GeneratedCodereturnsText.ToString().SetCSharpDocument(also in the old baseline) is intentionally not restored — it was a mutatingvoidon what is now an immutable type, and nothing consumes it; it should be dropped from the baseline instead.Correctness for the runtime path
Runtime compilation only handles MVC
.cshtml(Legacy) views; the markup split phase early-returns for non-components, so no declaration half is produced and the implementation document is the complete, standalone-compilable class. Blazor components are build-time only (the source generator emits decl + impl as two partials into one compilation), so nothing compiles a component through this public runtime accessor. A probe over the default engine confirms.cshtml→ complete non-partial class, 0 diagnostics;.razor→ the implementation partial half.Validation
Microsoft.CodeAnalysis.Razor.Compilerbuilds clean (netstandard2.0 + net10.0), 0 warnings.Microsoft.AspNetCore.Mvc.Razor.ExtensionsCodeGenerationIntegrationTest: 48/48 (these compile.cshtmlgenerated code to a real assembly — the runtime-compilation path).Microsoft Reviewers: Open in CodeFlow