Skip to content

Add opt-in ~/ asset-path expansion for Razor components (compiler) - #84796

Merged
chsienki merged 6 commits into
dotnet:mainfrom
chsienki:features/tilde-path-expansion
Aug 18, 2026
Merged

Add opt-in ~/ asset-path expansion for Razor components (compiler)#84796
chsienki merged 6 commits into
dotnet:mainfrom
chsienki:features/tilde-path-expansion

Conversation

@chsienki

@chsienki chsienki commented Aug 6, 2026

Copy link
Copy Markdown
Member

This is the Roslyn (Razor compiler) half of the ~/ asset-path expansion feature proposed in dotnet/aspnetcore#68229.

Note

The feature is inert until the runtime ships [AssetPath] / [AcceptsAssetPath] / the AssetPathAttributes convention type in Microsoft.AspNetCore.Components. The tests stub those attributes, so the compiler builds and its tests pass today, but end-to-end behavior depends on the coordinated runtime + SDK changes landing.

What this does

Adds opt-in, compile-time expansion of literal ~/-prefixed attribute values in .razor files into Assets[@"..."] expressions, so <img src="~/images/logo.png"> behaves like <img src="@Assets[@"images/logo.png"]"> -- only where the target has explicitly opted in:

  • HTML element attributes whose (element, attribute) pair the runtime declares via [AcceptsAssetPath(elementName, attributeName)] on a public AssetPathAttributes convention type (built-in allowlist: img[src], link[href], script[src]).
  • Component parameters marked [AssetPath] (honored only on string parameters).

When nothing is opted in, ~/ is left untouched -- existing markup is unaffected. There is no ambient behavior, so no MSBuild opt-out is needed.

Design

  • AcceptsAssetPathTagHelperProducer discovers the AssetPathAttributes convention type during tag-helper discovery and emits carrier TagHelperDescriptors (AssetPathMetadata) with no tag-matching rules -- they exist purely to carry the allowlist to the lowering pass. [AssetPath] on a string component parameter is recorded as PropertyMetadata.AcceptsAssetPath.
  • ComponentTildePathPass (an IRazorOptimizationPass, Order 75) reads the full discovered tag-helper set via ITagHelperFeature (compilation-global, not scoped to the document's in-scope tag helpers), builds an element -> attributes allowlist once per engine, and rewrites only opted-in single-literal ~/ values. Gated on Razor language version 11.0.

Diagnostics

  • RZ10029 (warning): a ~/ literal is mixed with dynamic content on an opted-in attribute, so it can't be expanded.
  • RZ10030 (warning): [AssetPath] is applied to a non-string parameter; the opt-in is ignored (expanding ~/ would yield an Assets[...] string that isn't assignable).

Commits

  1. Add asset-path metadata types (enums + AssetPathMetadata + PropertyMetadata.AcceptsAssetPath).
  2. Discover [AssetPath] / [AcceptsAssetPath] declarations (producer + registration; RZ10030 for non-string [AssetPath]).
  3. Expand opted-in ~/ literals via ComponentTildePathPass (+ RZ10029).
  4. Tests and baselines.
  5. Docs (docs/razor/tilde-path-expansion.md).

Each commit builds and is test-green.

Testing

ComponentCodeGenerationTestBase TildePath_* cases cover opted-in expansion (HTML + component params) and the backward-compatible non-expansion cases: not opted in, attribute outside the allowlist, bare ~, explicit @(), pre-11.0 language version, mixed content (RZ10029), and non-string [AssetPath] (RZ10030).

Related

@chsienki
chsienki force-pushed the features/tilde-path-expansion branch 3 times, most recently from 4b70ac9 to 5bf0f6c Compare August 10, 2026 17:49
Introduce the descriptor and property metadata the Razor compiler uses to
recognize opt-in asset-path expansion, ahead of the discovery and lowering that
consume them:

- AssetPathMetadata carries a single (element, attribute) pair declared via
  [AcceptsAssetPath], surfaced as a tag-helper descriptor.
- PropertyMetadata.AcceptsAssetPath marks a component parameter opted in via
  [AssetPath].
- New AssetPath / AcceptsAssetPath members on the TagHelperKind, MetadataKind,
  and TagHelperProducerKind enums, with the component-kind range extended to
  include the new tag-helper kind.
- ComponentsApi gains the well-known metadata names for the [AssetPath] and
  [AcceptsAssetPath] attributes and the AssetPathAttributes convention type.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
@chsienki
chsienki force-pushed the features/tilde-path-expansion branch from 5bf0f6c to 87fe605 Compare August 11, 2026 17:40
@chsienki
chsienki marked this pull request as ready for review August 11, 2026 17:40
@chsienki
chsienki requested a review from a team as a code owner August 11, 2026 17:40
Copilot AI lite review requested due to automatic review settings August 11, 2026 17:40
@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

Adds compiler-side support in the Razor component pipeline for opt-in expansion of literal attribute values starting with ~/ into Assets[...] expressions, using runtime-declared opt-in metadata ([AcceptsAssetPath] for specific HTML element/attribute pairs and [AssetPath] for component parameters). This lives in the Razor compiler layer (src/Razor/src/Compiler/...) and is gated on Razor language version 11.0.

Changes:

  • Introduces AcceptsAssetPathTagHelperProducer + AssetPathMetadata carriers and PropertyMetadata.AcceptsAssetPath to surface opt-in metadata into lowering/optimization.
  • Adds ComponentTildePathPass (Order 75) to rewrite opted-in ~/... literals into Assets[@"..."] tokens and emit warning RZ10029 for mixed content.
  • Adds extensive baseline coverage in ComponentCodeGenerationTestBase and new documentation describing the feature.

Reviewed changes

Copilot reviewed 99 out of 99 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/TagHelperProducerKind.cs Adds producer kind for [AcceptsAssetPath] discovery.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/ComponentTagHelperProducer.cs Records [AssetPath] on component parameters as AcceptsAssetPath metadata.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/AcceptsAssetPathTagHelperProducer.Factory.cs Factory to enable producer only when runtime attribute type is present.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelpers/Producers/AcceptsAssetPathTagHelperProducer.cs Produces carrier tag helpers containing (element, attribute) allowlist metadata.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperKindExtensions.cs Extends “component kinds” range to include AssetPath.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperKind.cs Adds new TagHelperKind.AssetPath value.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs Registers ComponentTildePathPass in the component feature set.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataObject.cs Adds MetadataKind.AssetPath for new metadata record type.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/PropertyMetadata.cs Adds AcceptsAssetPath flag to component-parameter metadata and checksum.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentTildePathPass.cs Implements the rewrite pass and mixed-content diagnostic emission.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentsApi.cs Adds metadata-name constants for new runtime attributes and ComponentBase.Assets.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentDiagnosticFactory.cs Introduces warning descriptor + factory for RZ10029 (mixed content).
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/AssetPathMetadata.cs New metadata record for (element, attribute) allowlist entries.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilerFeatures.cs Registers AcceptsAssetPathTagHelperProducer for discovery.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_WithSlash/TestComponent.ir.txt New baseline: IR for opted-in ~/ expansion (HTML).
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_WithSlash/TestComponent.decl.ir.txt New baseline: declaration IR for TildePath_WithSlash.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_WithSlash/TestComponent.decl.codegen.cs New baseline: declaration codegen for TildePath_WithSlash.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_WithSlash/TestComponent.codegen.cs New baseline: codegen for TildePath_WithSlash.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_WithSlash/TestComponent.builder.txt New baseline: render-tree builder output for TildePath_WithSlash.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NoTilde/TestComponent.ir.txt New baseline: non-expansion when no tilde.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NoTilde/TestComponent.decl.ir.txt New baseline: declaration IR for TildePath_NoTilde.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NoTilde/TestComponent.decl.codegen.cs New baseline: declaration codegen for TildePath_NoTilde.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NoTilde/TestComponent.codegen.cs New baseline: codegen for TildePath_NoTilde.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NoTilde/TestComponent.builder.txt New baseline: builder output for TildePath_NoTilde.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NotExpandedBeforeLanguageVersion11/TestComponent.ir.txt New baseline: version-gated non-expansion (pre-11.0).
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NotExpandedBeforeLanguageVersion11/TestComponent.decl.ir.txt New baseline: declaration IR for version-gated case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NotExpandedBeforeLanguageVersion11/TestComponent.decl.codegen.cs New baseline: declaration codegen for version-gated case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NotExpandedBeforeLanguageVersion11/TestComponent.codegen.cs New baseline: codegen for version-gated case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_NotExpandedBeforeLanguageVersion11/TestComponent.builder.txt New baseline: builder output for version-gated case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MultipleElements/TestComponent.ir.txt New baseline: multiple opted-in HTML attributes.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MultipleElements/TestComponent.decl.ir.txt New baseline: declaration IR for multiple-elements case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MultipleElements/TestComponent.decl.codegen.cs New baseline: declaration codegen for multiple-elements case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MultipleElements/TestComponent.codegen.cs New baseline: codegen for multiple-elements case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MultipleElements/TestComponent.builder.txt New baseline: builder output for multiple-elements case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.mappings.txt New baseline: source mappings for mixed-content scenario.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.ir.txt New baseline: IR showing mixed literal/expression attribute content.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.diagnostics.txt New baseline: RZ10029 warning for mixed content.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.decl.ir.txt New baseline: declaration IR for mixed-content case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.decl.codegen.cs New baseline: declaration codegen for mixed-content case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.codegen.cs New baseline: codegen for mixed-content case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_MixedContent/TestComponent.builder.txt New baseline: builder output for mixed-content case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement/TestComponent.ir.txt New baseline: HTML opted-in expansion (img[src]).
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement/TestComponent.decl.ir.txt New baseline: declaration IR for HTML opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement/TestComponent.decl.codegen.cs New baseline: declaration codegen for HTML opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement/TestComponent.codegen.cs New baseline: codegen for HTML opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement/TestComponent.builder.txt New baseline: builder output for HTML opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement_NotOptedIn/TestComponent.ir.txt New baseline: non-expansion when HTML allowlist isn’t present.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement_NotOptedIn/TestComponent.decl.ir.txt New baseline: declaration IR for HTML not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement_NotOptedIn/TestComponent.decl.codegen.cs New baseline: declaration codegen for HTML not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement_NotOptedIn/TestComponent.codegen.cs New baseline: codegen for HTML not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_HtmlElement_NotOptedIn/TestComponent.builder.txt New baseline: builder output for HTML not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.mappings.txt New baseline: mappings for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.ir.txt New baseline: IR for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.decl.ir.txt New baseline: declaration IR for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.decl.codegen.cs New baseline: declaration codegen for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.codegen.cs New baseline: codegen for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ExplicitExpressionNotExpanded/TestComponent.builder.txt New baseline: builder output for explicit-expression escape case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPathWithSlash/TestComponent.ir.txt New baseline: no expansion for ~/ with empty path.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPathWithSlash/TestComponent.decl.ir.txt New baseline: declaration IR for empty-path-with-slash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPathWithSlash/TestComponent.decl.codegen.cs New baseline: declaration codegen for empty-path-with-slash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPathWithSlash/TestComponent.codegen.cs New baseline: codegen for empty-path-with-slash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPathWithSlash/TestComponent.builder.txt New baseline: builder output for empty-path-with-slash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPath/TestComponent.ir.txt New baseline: no expansion for bare ~.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPath/TestComponent.decl.ir.txt New baseline: declaration IR for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPath/TestComponent.decl.codegen.cs New baseline: declaration codegen for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPath/TestComponent.codegen.cs New baseline: codegen for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_EmptyPath/TestComponent.builder.txt New baseline: builder output for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.mappings.txt New baseline: mappings for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.ir.txt New baseline: IR for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.decl.ir.txt New baseline: declaration IR for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.decl.codegen.cs New baseline: declaration codegen for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.codegen.cs New baseline: codegen for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam/TestComponent.builder.txt New baseline: builder output for component parameter expansion.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.mappings.txt New baseline: mappings for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.ir.txt New baseline: IR for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.decl.ir.txt New baseline: declaration IR for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.decl.codegen.cs New baseline: declaration codegen for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.codegen.cs New baseline: codegen for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_ComponentParam_NotOptedIn/TestComponent.builder.txt New baseline: builder output for component param non-expansion case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BareTildeNotExpanded/TestComponent.ir.txt New baseline: bare ~path does not expand.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BareTildeNotExpanded/TestComponent.decl.ir.txt New baseline: declaration IR for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BareTildeNotExpanded/TestComponent.decl.codegen.cs New baseline: declaration codegen for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BareTildeNotExpanded/TestComponent.codegen.cs New baseline: codegen for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BareTildeNotExpanded/TestComponent.builder.txt New baseline: builder output for bare-tilde case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BackslashInPath/TestComponent.ir.txt New baseline: expansion preserves backslashes via verbatim literal.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BackslashInPath/TestComponent.decl.ir.txt New baseline: declaration IR for backslash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BackslashInPath/TestComponent.decl.codegen.cs New baseline: declaration codegen for backslash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BackslashInPath/TestComponent.codegen.cs New baseline: codegen for backslash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_BackslashInPath/TestComponent.builder.txt New baseline: builder output for backslash case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_AttributeNotOptedIn/TestComponent.ir.txt New baseline: non-expansion for attributes outside allowlist.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_AttributeNotOptedIn/TestComponent.decl.ir.txt New baseline: declaration IR for attribute-not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_AttributeNotOptedIn/TestComponent.decl.codegen.cs New baseline: declaration codegen for attribute-not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_AttributeNotOptedIn/TestComponent.codegen.cs New baseline: codegen for attribute-not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/TildePath_AttributeNotOptedIn/TestComponent.builder.txt New baseline: builder output for attribute-not-opted-in case.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs Updates default feature list assertions to include ComponentTildePathPass.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs Adds test region, stubs, and integration tests for tilde expansion scenarios.
docs/razor/tilde-path-expansion.md New end-to-end documentation describing syntax, opt-in model, diagnostics, and implementation.
.github/memory/testing/razor.md Updates Razor test guidance (baselines and two-phase compilation notes).
.github/instructions/Razor.instructions.md Documents the “runtime-declared attribute list” pattern for Razor compiler work.
Suppressed comments (2)

docs/razor/tilde-path-expansion.md:22

  • The docs’ generated C# snippet uses Assets["images/logo.png"], but the compiler emits a verbatim literal (Assets[@"images/logo.png"]). The snippet should match actual output (including the @).
Both produce the same generated C#:

```csharp
__builder.AddAttribute(1, "src", Assets["images/logo.png"]);
**docs/razor/tilde-path-expansion.md:62**
* In the Syntax table, the “Generated C#” column shows `Assets["..."]`, but the compiler emits verbatim literals (`Assets[@"..."]`). Updating the table keeps it consistent with actual codegen and with the backslash-handling rationale later in the doc.
Input Generated C# Notes
src="~/images/logo.png" Assets["images/logo.png"] Standard form
src="~images/logo.png" "~images/logo.png" (literal) No slash, no transformation
src="~/~images.png" Assets["~images.png"] Only leading ~/ is special
</details>

Comment thread docs/razor/tilde-path-expansion.md
chsienki and others added 4 commits August 11, 2026 11:12
Read the runtime's asset-path opt-in metadata during tag-helper discovery:

- AcceptsAssetPathTagHelperProducer finds the public AssetPathAttributes
  convention type and emits one carrier tag-helper descriptor per declared
  (element, attribute) pair, following the BindAttributes / EventHandlers model.
  The descriptors declare no tag-matching rules; they exist only to carry the
  allowlist to the lowering pass.
- ComponentTagHelperProducer records [AssetPath] on a component parameter as
  PropertyMetadata.AcceptsAssetPath.
- CompilerFeatures registers the new producer.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
Rewrite literal ~/-prefixed attribute values into Assets["..."] expressions, but
only where the target has opted in:

- HTML element attributes whose (element, attribute) pair appears in the
  discovered [AcceptsAssetPath] allowlist.
- Component parameters whose bound attribute has PropertyMetadata.AcceptsAssetPath.

The pass reads the full discovered tag-helper set from ITagHelperFeature so the
allowlist is compilation-global rather than scoped to the document's in-scope tag
helpers. Mixed literal/expression content on an opted-in attribute is reported as
RZ10025. Expansion is gated on Razor language version 11.0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
Cover opted-in expansion for HTML elements and component parameters, plus the
backward-compatible cases where ~/ is left untouched: no [AssetPath], an
attribute outside the allowlist, bare ~, explicit @() expressions, pre-11.0
language version, and mixed content (RZ10025).

Baselines are generated with:
  dotnet test src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Microsoft.AspNetCore.Razor.Language.UnitTests.csproj /p:GenerateBaselines=true
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
Add the feature design doc and record the discovery/allowlist pattern and the
baseline-regeneration workflow in the agent knowledge base.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
Copilot AI review requested due to automatic review settings August 11, 2026 18:16
@chsienki
chsienki force-pushed the features/tilde-path-expansion branch from 87fe605 to d0f1c32 Compare August 11, 2026 18:16

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

Copilot reviewed 99 out of 99 changed files in this pull request and generated no new comments.

@chsienki

Copy link
Copy Markdown
Member Author

/azp run roslyn-CI

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

}

// Verbatim string literal: backslashes (common in paths) stay literal and only embedded
// quotes need doubling. Source is null -- synthetic code with no .razor mapping.

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.

This is interesting. Tooling supports Go To Def on file paths in string literals, we will have to follow up here because the existing system won't work without mapping. We probably can't even use an enhanced line pragma to map part of the string, right, because of the trimmed ~/?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I opened #84945 to track this

- Convert the AssetPathMetadata and ComponentTildePathPass header comments to
  <summary> doc comments, and move the BuildAllowedElementAttributes note inside
  the method body.
- Use a list pattern in GetSingleLiteralToken.
- Add TildePath_MultipleAttributesSameElement covering two opted-in attributes on
  a single element (img srcset + src), and opt img[srcset] into the test stub.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef56ab10-2d3d-4e55-a318-3d03bfbdd713
Copilot AI review requested due to automatic review settings August 18, 2026 18:52

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

Copilot reviewed 101 out of 101 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs:13489

  • Comment says the backslash should be "escaped" in generated C#, but the generated code uses a verbatim string literal (e.g., @"images\logo.png"), so backslashes are preserved as-is. Update the comment to match the actual behavior being asserted by the baselines.
        // Assert - backslash should be escaped in the generated C# string literal

docs/razor/tilde-path-expansion.md:49

  • This public doc includes internal/non-public references (Azure DevOps board entry and rows marked "Private"), which aren’t actionable for external contributors and will quickly go stale. Consider removing these rows (or replacing them with public links) and keeping only publicly accessible references.
| aspnetcore#68229 | https://github.com/dotnet/aspnetcore/issues/68229 | **Canonical proposal** -- authoritative API names, HTML allowlist, and cross-repo plan |
| aspnetcore#56076 | https://github.com/dotnet/aspnetcore/issues/56076 | Runtime APIs that introduced `Assets` indexer |
| AzDO#2623010 | DevDiv Razor Experiences board | Internal tracking work item |
| Razor-Language-Design#10 | Private | Language design discussion |
| aspnet/specs#769 | Private | Javier's design note |

@chsienki
chsienki merged commit 6b1f505 into dotnet:main Aug 18, 2026
22 checks passed

// [AssetPath] enables ~/ expansion only for string parameters. On any other type the
// rewritten Assets[...] value wouldn't be assignable, so ignore the opt-in and warn.
if (property.GetAttributes().Any(static a => a.HasFullName(ComponentsApi.AssetPathAttribute.MetadataName)))

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.

@chsienki This doesn't consider inheritance right?

The attribute per the approved API in dotnet/aspnetcore#68237 uses Inherited = true

I do believe that either we should change it to Inherited = false or otherwise make sure that Razor respects inheritance correctly to avoid confusion.

chsienki added a commit that referenced this pull request Sep 9, 2026
`AssetPathAttribute` has `Inherited = true`, but component parameter
discovery only inspects attributes declared directly on the selected
property. An override that redeclares `[Parameter]` therefore loses the
base property's asset-path opt-in and leaves `~/` literals unexpanded.

Follow the `OverriddenProperty` chain when resolving `[AssetPath]`. This
preserves attribute inheritance for overrides without applying the
opt-in to hidden properties. Add an integration baseline covering a base
virtual parameter with `[AssetPath]` and a derived override that
redeclares only `[Parameter]`.

Addresses
#84796 (comment)

###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/roslyn/pull/85171)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd15ad81-c929-4d0c-91de-354c0c777533
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.

6 participants