Skip to content

Restore public GetCSharpDocument() and GeneratedCode for runtime compilation - #84813

Merged
ViktorHofer merged 1 commit into
dotnet:mainfrom
chsienki:fix/rzc-public-api-compat
Aug 9, 2026
Merged

ViktorHofer merged 1 commit into
dotnet:mainfrom
chsienki:fix/rzc-public-api-compat

Conversation

@chsienki

@chsienki chsienki commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

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-aware RazorCodeDocument.GetCSharpDocument(bool declarationDocument).
  • RazorCSharpDocument.GeneratedCode (string) → replaced by public SourceText Text.

RuntimeViewCompiler.CompileAndEmit calls codeDocument.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)).
  • GeneratedCode returns Text.ToString().

SetCSharpDocument (also in the old baseline) is intentionally not restored — it was a mutating void on 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.Compiler builds clean (netstandard2.0 + net10.0), 0 warnings.
  • Microsoft.AspNetCore.Mvc.Razor.Extensions CodeGenerationIntegrationTest: 48/48 (these compile .cshtml generated code to a real assembly — the runtime-compilation path).
Microsoft Reviewers: Open in CodeFlow

…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
Copilot AI lite review requested due to automatic review settings August 8, 2026 01:01
@chsienki
chsienki requested a review from a team as a code owner August 8, 2026 01:01
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.GeneratedCode as a shim over Text.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.

Comment on lines +25 to +26
public static RazorCSharpDocument GetCSharpDocument(this RazorCodeDocument codeDocument)
=> codeDocument.GetRequiredCSharpDocument(declarationDocument: false);
Comment on lines +21 to +23
/// 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.
@chsienki

chsienki commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Long-term follow-up to remove these compat shims (after migrating ASP.NET Core runtime compilation off them): #84814

@davidwengier davidwengier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@davidwengier

Copy link
Copy Markdown
Member

Nevermind, read the PR description. I didn't realise the SDK didn't handle .razor documents.

@ViktorHofer
ViktorHofer merged commit 9f2738a into dotnet:main Aug 9, 2026
27 checks passed
@ViktorHofer

Copy link
Copy Markdown
Member

Merging to unblock VMR Code flow.

@chsienki

Copy link
Copy Markdown
Member Author

Nevermind, read the PR description. I didn't realise the SDK didn't handle .razor documents.

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.

JoeRobich pushed a commit that referenced this pull request Aug 11, 2026
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)
chsienki added a commit that referenced this pull request Aug 21, 2026
…#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.
@jjonescz jjonescz added this to the 18.11 milestone Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants