Skip to content

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

Draft
chsienki wants to merge 5 commits into
dotnet:mainfrom
chsienki:features/tilde-path-expansion
Draft

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

Conversation

@chsienki

@chsienki chsienki commented Aug 6, 2026

Copy link
Copy Markdown
Member

Draft / coordination PR. This is the Roslyn (Razor compiler) half of the ~/ asset-path expansion feature proposed in dotnet/aspnetcore#68229. It builds and its tests pass today (the tests stub the runtime attributes), but the feature is inert until the runtime ships [AssetPath] / [AcceptsAssetPath] / the AssetPathAttributes convention type in Microsoft.AspNetCore.Components. Opening as draft to review the compiler design while the runtime + SDK pieces land.

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"> becomes <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].

When nothing is opted in, ~/ is left untouched -- existing markup is unaffected (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 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. Mixed literal/expression content on an opted-in attribute is reported as RZ10025. Gated on Razor language version 11.0.

Commits

  1. Add asset-path metadata types (enums + AssetPathMetadata + PropertyMetadata.AcceptsAssetPath)
  2. Discover [AssetPath] / [AcceptsAssetPath] declarations (producer + registration)
  3. Expand opted-in ~/ literals via ComponentTildePathPass (+ RZ10025)
  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, mixed content). 15/15 green.

Related

Microsoft Reviewers: Open in CodeFlow

chsienki and others added 5 commits August 6, 2026 16:35
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
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
@chsienki
chsienki force-pushed the features/tilde-path-expansion branch from 7eed863 to ba53e6b Compare August 7, 2026 00:53
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.

1 participant