dotnet / dotnet/roslyn

Support compile-time propagation of Web Component slot attributes through RenderFragment roots

Open
#85,296 2 comments 3 reactions 0 assignees View on GitHub
Area-Language Design Area-Razor Area-Razor-Compiler untriaged
Dominant language
C#
Stars
20.7k
Forks
4.3k
PR merge metrics
PR metrics pending

Description

## Background

ASP.NET Core issue [dotnet/aspnetcore#68954](https://github.com/dotnet/aspnetcore/issues/68954) describes a gap when a Razor component wraps a Web Component with named slots.

A wrapper commonly exposes named content as a `RenderFragment`:

```csharp
[Parameter]
public RenderFragment? Icon { get; set; }
```

and consumers provide ordinary Razor content:

```razor


settings

Save

```

The Web Component requires the rendered light-DOM root to carry the native `slot` attribute:

```html

settings
Save

```

Wrapping the fragment in an element does not preserve these semantics because the wrapper becomes the assigned slotted node. A `RenderFragment` is opaque at runtime, so the wrapper cannot add the attribute to its roots using supported APIs.

## Proposal

Add declarative metadata to a `RenderFragment` component parameter so the Razor compiler can propagate a Web Component slot assignment while generating the fragment.

The API names below are illustrative:

```csharp
namespace Microsoft.AspNetCore.Components;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SlotAttribute : Attribute
{
public SlotAttribute(string name);

public string Name { get; }
}

public interface ISlotReceiver
{
string? Slot { get; set; }
}
```

A wrapper would declare:

```csharp
[Parameter]
[Slot("icon")]
public RenderFragment? Icon { get; set; }
```

For inline Razor child content, the compiler would apply the following rules to each statically known top-level root:

1. For an HTML element, emit `slot="icon"` directly on the element.
2. For a component, pass the slot value to the component.
3. For roots inside compiler-visible control flow such as `@if` and `@foreach`, apply the same rules in each branch or iteration.
4. For multiple roots, apply the slot to every root.
5. Ignore formatting whitespace and emit nothing for an empty fragment.

Conceptually, this:

```razor


settings
alternate

```

would generate fragment code equivalent to:

```csharp
builder.OpenElement(0, "span");
builder.AddAttribute(1, "slot", "icon");
builder.AddContent(2, "settings");
builder.CloseElement();

builder.OpenComponent(3);
builder.AddAttribute(4, "slot", "icon");
builder.AddContent(5, "alternate");
builder.CloseComponent();
```

## Component forwarding

A Blazor component has no corresponding DOM node, so a slot assignment passed to a component must be forwarded until it reaches the component's top-level HTML roots.

For Razor-generated components deriving from `ComponentBase`, implementing `ISlotReceiver` would opt into compiler-generated behavior. The Razor compiler would provide the slot parameter implementation and propagate its value to that component's statically known top-level roots using the same rules above.

This makes propagation local to each compilation unit:

- A parent fragment passes the slot to an immediate component root.
- The receiving Razor component passes it to its immediate roots.
- Propagation continues through participating components until an HTML element receives the native attribute.

Components for which Razor cannot generate the implementation, including custom `IComponent` implementations, would implement the interface and forwarding behavior themselves. An explicit `Slot` parameter or unmatched-attribute handling could also provide a manual implementation path.

## Unsupported and diagnostic cases

The feature should not inspect or rewrite a rendered tree at runtime. Consequently, the compiler should report a diagnostic when it cannot prove how to propagate the slot, including cases such as:

```razor
@existingFragment
```

or:

```razor

```

Other cases requiring defined diagnostics include:

- A non-whitespace text or markup root, because it cannot carry a named `slot` attribute.
- A component root that cannot receive or forward the slot value.
- A root that already specifies a conflicting `slot` value.

## Non-goals

- Traversing the component tree or render tree at runtime.
- Invoking a fragment into a scratch `RenderTreeBuilder` and replaying or mutating its frames.
- Exposing `RenderTreeFrame` as a supported application-level mutation API.
- Supporting arbitrary opaque `RenderFragment` values whose roots are not visible to the compiler.

The intent is an 80/20 compiler feature for inline Razor markup, with explicit opt-in propagation through Razor components and clear diagnostics when compile-time propagation is not possible.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the Razor compiler entry points involved in generating RenderFragment content and component parameters. Define how SlotAttribute and ISlotReceiver should participate in propagation, including control flow, forwarding, and unsupported roots. Done means inline roots receive the slot value at compile time and clear diagnostics cover opaque fragments, conflicts, and non-forwarding components.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.