Host the Razor source generator in rzc (opt-in generate/discover)#55422
Host the Razor source generator in rzc (opt-in generate/discover)#55422chsienki wants to merge 5 commits into
Conversation
The rzc generate and discover commands invoke the Razor engine
(RazorProjectEngine) directly, a separate invocation path from the Razor
source generator that compiles Razor for .NET 6 and later. Add the shared
pieces needed to host that source generator in-process instead:
- SourceGeneratorSwitch gates the hosted path behind the
RAZOR_TOOL_USE_SOURCE_GENERATOR environment variable so it can be
validated against the engine path before becoming the default.
- RazorSourceGeneratorHost translates the tool's inputs into the generator's
inputs (analyzer config options, additional texts, and a reference-only
compilation) and creates the generator driver.
- RazorSourceGeneratorHostOutput isolates the single place that reads the
generator's internal RazorGeneratorResult host output, reached through the
compiler's InternalsVisibleTo("rzc") grant and its experimental
host-output API.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1cef513d-6587-488a-88e3-48059f1a54b2
When RAZOR_TOOL_USE_SOURCE_GENERATOR is set, both commands drive the Razor source generator rather than the engine: - generate builds a compilation from the reference assemblies passed via -a, runs the generator, and writes each generated document to the output path of the source it was produced from, mapping hint name to source path through the generator's host output. Declaration-only generation (--generate-declaration) stays on the engine path because the generator does not expose the intermediate declaration as an emitted source. - discover runs the generator over the reference assemblies together with a synthetic empty view, which the generator requires before it performs reference tag helper discovery, and serializes the discovered tag helpers to the existing manifest format. The engine path is used unchanged when the switch is off. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cef513d-6587-488a-88e3-48059f1a54b2
…tor mode In source-generator mode the tool discovers tag helpers from the compilation instead of a precomputed manifest, so the generate task must provide the project's references. SdkRazorGenerate forwards @(RazorReferencePath) as -a arguments, gated by UseSourceGenerator so the engine path is unaffected. A single opt-in property, defaulting from the RAZOR_TOOL_USE_SOURCE_GENERATOR environment variable, drives both the task's argument emission and the tool itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cef513d-6587-488a-88e3-48059f1a54b2
|
Azure Pipelines: Successfully started running 1 pipeline(s). 2 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
…operty Select the source-generator-hosted path with a --use-source-generator switch on the generate and discover commands, driven by the _RazorToolUseSourceGenerator MSBuild property (default false, so the engine path is unchanged). A per-invocation command-line argument is the correct shape for this toggle: under the Razor build server the long-lived server process handles requests from multiple projects, so the choice must travel with each request rather than being fixed in the server's environment when it starts. SdkRazorGenerate and SdkRazorTagHelper forward the switch to the tool (and, for generate, the reference assemblies the source generator needs) when the property is set; the property can be toggled with /p:_RazorToolUseSourceGenerator=true or in a project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1cef513d-6587-488a-88e3-48059f1a54b2
|
Azure Pipelines: Successfully started running 1 pipeline(s). 2 pipeline(s) were filtered out due to trigger conditions. 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 modernizes the legacy Razor CLI tool (rzc) by adding an opt-in path for generate and discover to host the Razor source generator in-process, aligning pre-.NET6 Razor builds with the .NET6+ source-generator pipeline. It also wires MSBuild tasks/targets to pass reference assemblies needed for compilation-based tag helper discovery when running in this mode.
Changes:
- Added in-process hosting infrastructure for the Razor source generator, including access to generator “host outputs” (
RazorGeneratorResult). - Routed
rzc generateandrzc discoverthrough the hosted source generator when--use-source-generatoris specified (with the engine path preserved as the default). - Updated Razor MSBuild tasks/targets to forward
--use-source-generatorand pass reference assemblies (-a) to enable compilation-based tag helper discovery.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/RazorSdk/Tool/RazorSourceGeneratorHostOutput.cs | Isolates access to experimental Roslyn host output API to retrieve RazorGeneratorResult. |
| src/RazorSdk/Tool/RazorSourceGeneratorHost.cs | Provides shared infrastructure for options/additional texts/compilation/driver creation for hosting the SG. |
| src/RazorSdk/Tool/GenerateCommand.cs | Adds --use-source-generator + -a and implements SG-backed generation path. |
| src/RazorSdk/Tool/DiscoverCommand.cs | Adds --use-source-generator and implements SG-backed tag helper discovery path. |
| src/RazorSdk/Tasks/SdkRazorTagHelper.cs | Forwards --use-source-generator to rzc discover. |
| src/RazorSdk/Tasks/SdkRazorGenerate.cs | Forwards --use-source-generator and reference assemblies (-a) to rzc generate. |
| src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets | Introduces _RazorToolUseSourceGenerator toggle and wires it into the tag helper + generation tasks. |
| var runResult = RazorSourceGeneratorHost.CreateDriver(parseOptions) | ||
| .AddAdditionalTexts(additionalTexts) | ||
| .WithUpdatedAnalyzerConfigOptions(optionsProvider) | ||
| .RunGeneratorsAndUpdateCompilation(compilation, out _, out _) | ||
| .GetRunResult().Results.Single(); | ||
|
|
||
| if (!RazorSourceGeneratorHostOutput.TryGet(runResult, out var razorResult)) | ||
| { | ||
| Error.WriteLine("The Razor source generator did not produce the expected host output."); | ||
| return ExitCodeFailure; | ||
| } |
| private int ExecuteWithSourceGenerator(SourceItem[] sourceItems) | ||
| { | ||
| var parseOptions = RazorSourceGeneratorHost.CreateParseOptions(GetCSharpLanguageVersion()); | ||
| var compilation = RazorSourceGeneratorHost.CreateCompilation(Assemblies.Values, Parent.AssemblyReferenceProvider); | ||
|
|
| var optionsProvider = RazorSourceGeneratorHost.CreateOptionsProvider( | ||
| razorConfiguration: Configuration.Value(), | ||
| razorLanguageVersion: Version.Value(), | ||
| rootNamespace: RootNamespace.Value(), | ||
| supportLocalizedComponentNames: SupportLocalizedComponentNames.HasValue(), | ||
| generateMetadataSourceChecksumAttributes: false, | ||
| projectDirectory: ProjectDirectory.Value(), | ||
| files: inputFiles); |
| <!-- Opt-in: route the Razor tool's discover and generate steps through the hosted source | ||
| generator. Toggle with /p:_RazorToolUseSourceGenerator=true or a project property; the | ||
| tasks forward it to the tool's use-source-generator switch. --> | ||
| <_RazorToolUseSourceGenerator Condition="'$(_RazorToolUseSourceGenerator)' == ''">false</_RazorToolUseSourceGenerator> | ||
| </PropertyGroup> |
| // The source generator path handles ordinary generation. Declaration-only generation | ||
| // (--generate-declaration) has no public generator output, so it stays on the engine path. | ||
| if (UseSourceGenerator.HasValue() && !GenerateDeclaration.HasValue()) | ||
| { | ||
| return Task.FromResult(ExecuteWithSourceGenerator(sourceItems)); | ||
| } |
davidwengier
left a comment
There was a problem hiding this comment.
Opening as a draft for design review — not intended to merge as-is.
No you didn't, so I reviewed it 😛
Generally LGTM, though I didn't see a change that I was expecting: Keeping discover and generate commands working individually makes sense (for now), I would have thought there would be a change such that if using the generator, the discover command is not run at all. What am I missing?
| { | ||
| outputFilePath = Path.Combine(projectDirectory, outputFilePath); | ||
|
|
||
| var parseOptions = RazorSourceGeneratorHost.CreateParseOptions(LanguageVersion.Default); |
There was a problem hiding this comment.
I'm curious if this is a change, in a scenario where Roslyn DLLs come from .NET 10, but the TFM is .NET 8? This would be C# 14 but perhaps the old way is 12?
It's kind of weird in that right now the pipeline will call I suppose we could probably just make discover return nothing in SG mode, I just wanted to keep the break as small as possible. But thinking about it I can't see that what's discovered is used anywhere else, so it's probably fine. |
Summary
Modernizes the legacy Razor CLI tool (
rzc) so itsgenerateanddiscovercommands can host the Razor source generator in-process instead of calling the Razor engine (RazorProjectEngine) directly. This keeps the pre-.NET 6 Razor build path (rzc, used for .NET Standard / .NET Framework / sub-net6 target frameworks) on the same, actively-maintained code generation as the .NET 6+ source-generator build, rather than a separate engine-invocation path.The new path is opt-in behind the
_RazorToolUseSourceGeneratorMSBuild property (default off) — set/p:_RazorToolUseSourceGenerator=true, or the property in a project, to enable it — so this is a no-op for existing builds. It is a reviewable prototype ahead of a later cutover.What's here (4 commits)
RazorGeneratorResulthost output (reached via the compiler's existingInternalsVisibleTo("rzc")grant).--generate-declarationstays on the engine (see below).SdkRazorGenerateforwards@(RazorReferencePath)as-a.generateanddiscovertake a--use-source-generatorswitch driven by the_RazorToolUseSourceGeneratorproperty (forwarded bySdkRazorGenerateandSdkRazorTagHelper). A per-invocation argument is the correct shape under the Razor build server, where the long-lived server serves multiple projects and the choice must travel with each request rather than being fixed in the server's environment.Validation
rzcbuilds clean (0/0); each commit builds green (bisect-safe).discover: the SG manifest is byte-identical to the engine's for the same references, and more robust (no MVC extension plumbing required).generate: modern-but-equivalent output (enhanced#linedirectives, SHA-1 checksum); the generated C# compiles against real ASP.NET Core references.UseRazorSourceGenerator=false) build on both the engine and SG paths, toggled with/p:_RazorToolUseSourceGenerator=true. The SG-built web app runs and renders pages correctly (HTTP 200,WebApp.Views.dllwith the expectedRazorCompiledItemAttribute).Declaration generation (
--generate-declaration)Component compilation on the legacy path runs a declaration pass (
rzc generate --generate-declaration) that emits each component's type surface — the class and its[Parameter]members, with no render body — into a throwaway assembly so components can discover one another as tag helpers before full generation.The source generator produces this declaration internally but never exposes it as a consumable output: the declaration entry point is
private, and the declaration text is only added to the in-memory compilation for discovery — it is never emitted as a generated source or host output. So when the SG path is enabled, the declaration pass falls back to the engine while full generation is SG-hosted.This is fine for now. A declaration is only a type surface and doesn't rely on any newer code-generation features, so for a given Razor language version the engine-produced declaration is equivalent to what the SG would produce — and both use the same Razor compiler version here. Longer term we intend to extend the SG to expose full declaration output so
rzccan produce it there too, but that part of the generator is actively changing as part of Project Sonic, so for now we keep the fallback.Not in this PR / follow-ups
--generate-declarationis also SG-hosted.Draft
Opening as a draft for design review — not intended to merge as-is.